JNI
JNIEnv
asynchronous calls
Java Native Interface
programming tutorials

How to obtain JNI interface pointer JNIEnv for asynchronous calls

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

JNIEnv* is thread-local. That single fact explains almost every correct answer to asynchronous JNI usage: you must not cache a JNIEnv* from one thread and use it from another thread later. For asynchronous native threads, the correct pattern is to cache JavaVM*, attach the current thread to the JVM when needed, get a thread-specific JNIEnv*, do the JNI work, and detach when finished.

Why You Cannot Reuse JNIEnv* Across Threads

When native code is called from Java, the current thread already has a valid JNIEnv* and it is passed as a parameter.

For example:

c
1JNIEXPORT void JNICALL
2Java_com_example_NativeLib_doWork(JNIEnv* env, jobject thiz) {
3    /* env is valid only for this calling thread */
4}

That pointer is only valid for that attached thread. If you store it globally and use it later on a different worker thread, behavior is undefined and crashes are common.

So the correct reusable object is JavaVM*, not JNIEnv*.

Cache JavaVM* In JNI_OnLoad

A common pattern is to save the VM pointer when the library loads.

c
1#include <jni.h>
2
3static JavaVM* g_vm = NULL;
4
5JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
6    g_vm = vm;
7    return JNI_VERSION_1_6;
8}

JavaVM* is process-wide for the JVM instance and can be used later to attach native threads.

Attach The Native Thread When Needed

If an asynchronous native thread needs to call Java, attach it first.

c
1#include <jni.h>
2#include <pthread.h>
3#include <stdio.h>
4
5extern JavaVM* g_vm;
6
7void* worker_thread(void* arg) {
8    JNIEnv* env = NULL;
9    int must_detach = 0;
10
11    if ((*g_vm)->GetEnv(g_vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) {
12        if ((*g_vm)->AttachCurrentThread(g_vm, (void**)&env, NULL) != JNI_OK) {
13            fprintf(stderr, "AttachCurrentThread failed\n");
14            return NULL;
15        }
16        must_detach = 1;
17    }
18
19    /* safe JNI calls using env go here */
20
21    if (must_detach) {
22        (*g_vm)->DetachCurrentThread(g_vm);
23    }
24    return NULL;
25}

This is the standard asynchronous JNI pattern.

Use Global References For Java Objects

If the worker thread needs to keep a Java object reference after the original JNI call returns, create a global reference. Local references are only valid for the duration of the native call on that thread.

c
jobject global_callback = (*env)->NewGlobalRef(env, callback_obj);

Later, from the attached worker thread, you can use that global reference safely with the new thread-specific JNIEnv*.

When done:

c
(*env)->DeleteGlobalRef(env, global_callback);

This is just as important as the AttachCurrentThread step. Correct thread attachment does not make local references magically long-lived.

A Typical End-To-End Pattern

A common architecture looks like this:

  1. Java calls native code on a JVM-managed thread
  2. native code stores JavaVM* if not already stored
  3. native code creates any needed global references
  4. native code starts a worker thread
  5. worker thread attaches to the JVM
  6. worker thread obtains its own JNIEnv*
  7. worker thread invokes Java callbacks or manipulates Java objects
  8. worker thread deletes global refs when appropriate and detaches

That sequence is boring, but boring is what you want in JNI lifecycle code.

Beware Of Thread Leaks And Attach Cost

Attaching and detaching threads has overhead. If your design creates thousands of short-lived native threads just to make tiny JNI calls, the architecture may be wrong.

In many cases, using a smaller persistent worker pool is better than continuously creating new native threads.

Also, always detach threads you attached yourself. Otherwise the JVM keeps thread-related resources alive longer than intended.

A Java Callback Example Outline

Suppose you cached a jmethodID and a global callback object. Then your attached worker thread can call back into Java like this:

c
1(*env)->CallVoidMethod(env, global_callback, callback_method, 42);
2if ((*env)->ExceptionCheck(env)) {
3    (*env)->ExceptionDescribe(env);
4    (*env)->ExceptionClear(env);
5}

Checking for Java exceptions after the callback is a good habit because JNI does not throw C exceptions for you.

Common Pitfalls

  • Caching JNIEnv* globally and reusing it on another thread.
  • Forgetting to call DetachCurrentThread for native threads that were manually attached.
  • Using local references after the original JNI call has returned.
  • Starting many tiny native threads instead of reusing attached worker threads sensibly.
  • Ignoring pending Java exceptions after Call*Method JNI calls.

Summary

  • 'JNIEnv* is thread-specific and must not be shared across threads.'
  • Cache JavaVM*, not JNIEnv*, for asynchronous native work.
  • Attach the current native thread to the JVM to obtain a valid JNIEnv*.
  • Use global references for Java objects that must outlive the original JNI call.
  • Detach native threads when their JNI work is done.

Course illustration
Course illustration

All Rights Reserved.