上一篇解决的是一个问题当前是哪条线程ART 认不认识它我们最后得到了一条主线Native 自建线程 ↓ 如果要进入 Java 世界 ↓ AttachCurrentThread ↓ 获得当前线程可用的 JNIEnv到这里很容易产生一种错觉“线程 Attach 对了JNIEnv也拿到了那 JNI 多线程应该就安全了吧”其实还没有。因为线程正确只说明这条线程有资格正确地使用 JNI。但还有第二个问题你准备使用的那个 Java 对象引用现在还有效吗这就是LocalRef / GlobalRef要解决的问题。文档把它明确称为 JNI 多线程的第二条主线线程 Attach 正确并不代表你保存下来的 Java 对象引用可以一直使用。JNI多线程_从线程本质到真正理解透一、先从一个最普通的 callback 开始假设 Kotlin 有一个回调对象class MyCallback { fun onResult(value: Int) { println(result $value) } }然后传给 Nativeexternal fun nativeStart(callback: MyCallback) nativeStart(MyCallback())CJNIEXPORT void JNICALL nativeStart( JNIEnv* env, jobject thiz, jobject callback) { }现在先只看jobject callback它代表什么你可以先简单理解成C 手里拿到了一个指向 Java 对象的 JNI 引用。大概是Java Heap ┌─────────────────────┐ │ MyCallback 对象 │ └─────────────────────┘ ↑ │ jobject callback ↑ │ C那么问题来了这个callback拿到以后我是不是可以存起来以后想什么时候用就什么时候用不是。因为这里的callback通常属于Local Reference 局部引用二、LocalRef 是什么JNI 方法参数中的jobject jclass jstring以及很多 JNI API 返回的 Java 对象引用默认属于Local Reference。你的文档对它的描述是Local Reference 和当前线程的局部引用管理、当前 JNI 调用生命周期相关。JNI多线程_从线程本质到真正理解透暂时不用抠内部实现。先建立一个最实用的理解LocalRef 是“当前这次 JNI 调用里临时使用”的 Java 对象引用。例如JNIEXPORT void JNICALL nativeFoo( JNIEnv* env, jobject thiz, jobject callback) { env-CallVoidMethod( callback, methodId ); }整个调用过程Java │ │ nativeFoo(callback) ↓ C │ │ callback LocalRef │ │ 现在立即使用 ↓ nativeFoo 返回这种情况很正常。你在当前 JNI 调用里env-CallVoidMethod(callback, ...);使用这个对象。没有问题。三、真正的问题我把 callback 保存下来不就行了吗很多人第一次写 JNI 异步回调时会自然写出这种代码static jobject gCallback nullptr; JNIEXPORT void JNICALL nativeStart( JNIEnv* env, jobject thiz, jobject callback) { gCallback callback; }看起来非常合理callback ↓ 保存到 gCallback ↓ 以后再用但是这里有一个关键误区把一个 LocalRef 的值保存到全局变量里并不会把它自动变成长期有效的引用。也就是说gCallback callback;做的事情只是把这个 JNI 引用值保存了下来并不是把这个 Java 对象引用的生命周期延长了所以static jobject gCallback;虽然这个 C 变量本身是全局变量但gCallback 里面保存的那个 JNI 引用仍然可能只是原来的 LocalRef。文档也专门给出了这种错误形式static jobject gCallback; JNIEXPORT void nativeStart( JNIEnv* env, jobject callback) { gCallback callback; }nativeStart()返回以后未来异步使用这个引用是不安全的。JNI多线程_从线程本质到真正理解透四、为什么“存下来”不等于“活得更久”这里可以把C 变量生命周期和JNI 引用生命周期分开理解。比如static jobject gCallback;这个gCallback 变量确实可以活很久。但它里面保存的是一个JNI Reference这个引用本身有自己的生命周期规则。所以C 指针/变量还在 ≠ 它指向的 JNI 引用仍然有效你可以把它想成一张临时门票。LocalRef当前 JNI 调用有效你把门票编号抄到本子上A00123并不会让这张临时门票变成永久票。同样gCallback callback;只是把“编号”记下来了。没有改变它本来的生命周期。五、那我要异步使用怎么办这时候就需要NewGlobalRef()例如static jobject gCallback nullptr; JNIEXPORT void JNICALL nativeStart( JNIEnv* env, jobject thiz, jobject callback) { gCallback env-NewGlobalRef(callback); }这一步的意义就是基于当前 Java 对象创建一个可以跨当前 JNI 调用继续持有的 Global Reference。所以现在可以理解成Java Object ↑ │ GlobalRef ↑ │ C 长期持有文档里的标准形式就是gCallback env-NewGlobalRef(callback);未来异步线程使用完以后env-DeleteGlobalRef(gCallback);释放它。JNI多线程_从线程本质到真正理解透所以最简单的区别就是LocalRef → 当前 JNI 调用内临时使用 GlobalRef → 需要跨调用、异步、长期持有六、NewGlobalRef 会复制一个 Java 对象吗不会。这是另一个很容易误解的地方。假设 Java Heap 里有MyCallback 对象调用env-NewGlobalRef(callback);并不是MyCallback ↓ copy MyCallback2也不是重新 new 一个 Java 对象。更适合这样理解同一个 Java 对象 ↑ ┌───────┴───────┐ │ │ LocalRef GlobalRef也就是说对象还是那个对象只是 Native 侧现在获得了一种生命周期不同的引用。所以NewGlobalRef()重点是Reference不是Object Copy七、GlobalRef 能跨线程是不是就不需要 Attach 了这是这一篇最重要的分界。答案是不是。因为Attach / JNIEnv和LocalRef / GlobalRef解决的是两个完全不同的问题。你的文档用一句非常关键的话概括了线程能不能调用 Java →JavaVM / JNIEnv对象能不能活到未来 →LocalRef / GlobalRef。JNI多线程_从线程本质到真正理解透所以问题一 当前线程能不能进入 Java 世界 → JavaVM → Attach → JNIEnv而问题二 我要使用的这个 Java 对象引用 现在还有效吗 → LocalRef → GlobalRef两条线要完全分开。八、把两个问题放进同一个例子现在来一个最典型的场景。KotlinnativeStart(callback)CJNIEXPORT void JNICALL nativeStart( JNIEnv* env, jobject thiz, jobject callback) { std::thread([callback] { // 3 秒后调用 callback }).detach(); }乍一看只是创建一个线程 3 秒后回调 Java但这里其实同时存在两个问题。第一个问题新线程有没有 JNI 身份std::thread(...)创建了一条新的 Linux Thread。现在Linux 认识它 ✅ ART 不一定认识 ❌所以它如果要CallVoidMethod(...)需要先JavaVM ↓ AttachCurrentThread ↓ 获得当前线程 JNIEnv这是线程身份问题。第二个问题callback 还有效吗callback是nativeStart(...)参数。它默认是 LocalRef。但是nativeStart() 已经返回几秒以后新线程才去使用callback这个引用已经不应该被这样依赖。所以gCallback env-NewGlobalRef(callback);这是对象引用生命周期问题。于是正确思路开始变成Java │ │ callback ↓ nativeStart() │ ├─ callback 是 LocalRef │ ├─ NewGlobalRef(callback) │ ↓ │ gCallback │ └─ std::thread() ↓ Native Thread ↓ AttachCurrentThread ↓ 得到自己的 JNIEnv ↓ CallVoidMethod( gCallback ) ↓ DeleteGlobalRef ↓ DetachCurrentThread到这里你就能看到Attach和GlobalRef为什么经常一起出现。不是因为它们是一个东西。而是因为“Native 异步回调 Java”这个场景刚好同时需要解决两个问题新线程 → 身份问题 旧 callback → 生命周期问题九、完整代码现在就能看懂了文档里的完整异步回调就是这样。JNI多线程_从线程本质到真正理解透先保存static JavaVM* gVm nullptr; static jobject gCallback nullptr; static jmethodID gOnResult nullptr;JNI_OnLoadJNIEXPORT jint JNI_OnLoad( JavaVM* vm, void*) { gVm vm; return JNI_VERSION_1_6; }Java 调进来JNIEXPORT void JNICALL nativeStart( JNIEnv* env, jobject thiz, jobject callback) { gCallback env-NewGlobalRef(callback); jclass cls env-GetObjectClass(callback); gOnResult env-GetMethodID( cls, onResult, (I)V ); env-DeleteLocalRef(cls); std::thread([] { // 异步线程 }).detach(); }这里callback为什么要NewGlobalRef()现在已经很好理解了。因为nativeStart 返回以后 我们还要继续使用 callback所以 LocalRef 不够。异步线程JNIEnv* env nullptr; bool attachedHere false; if (gVm-GetEnv( reinterpret_castvoid**(env), JNI_VERSION_1_6) JNI_EDETACHED) { gVm-AttachCurrentThread( reinterpret_castvoid**(env), nullptr); attachedHere true; }这部分解决线程身份然后env-CallVoidMethod( gCallback, gOnResult, 100 );这里使用的是GlobalRef解决的是对象生命周期用完env-DeleteGlobalRef(gCallback); gCallback nullptr;最后if (attachedHere) { gVm-DetachCurrentThread(); }整个过程现在就可以拆成线程 → Attach 对象 → GlobalRef 调用 → CallVoidMethod 对象不用了 → DeleteGlobalRef 线程自己 Attach 的 → Detach十、DeleteGlobalRef 为什么必须做既然NewGlobalRef()让 Native 可以长期持有这个 Java 对象引用那就意味着你必须明确告诉 VM我什么时候不再需要这个 GlobalRef。所以env-DeleteGlobalRef(gCallback);不是可有可无的“清理代码”。而是GlobalRef 生命周期管理的一部分。整体是成对的NewGlobalRef ↓ 长期持有 ↓ 使用 ↓ DeleteGlobalRef否则你就一直把这个引用留在那里。所以可以把它和new / delete malloc / free这种“谁创建、谁负责结束生命周期”的思路类比着理解。但要注意这里管理的是JNI 全局引用不是直接deleteJava 对象。十一、LocalRef 也能 DeleteLocalRef可以。比如jclass cls env-GetObjectClass(callback); env-DeleteLocalRef(cls);为什么 LocalRef 不是“本来就会结束”吗正常、短小的 JNI 调用里LocalRef 有自己的局部生命周期管理。但是如果是一条长生命周期的 Attached Native Thread例如Native Thread ↓ Attach ↓ while (...) ↓ 不断创建 LocalRef就不能无限往里堆。文档特别提醒长生命周期的 Attached Native Thread 如果在线程循环里不断创建 LocalRef应主动DeleteLocalRef或者使用PushLocalFrame / PopLocalFrame不要一直等到线程 Detach 才统一处理。JNI多线程_从线程本质到真正理解透所以LocalRef虽然是局部生命周期也不代表“我永远不用管。”特别是大量、循环、长时间运行的 JNI 场景。十二、GlobalRef 可以跨线程但还有第三个问题现在假设Thread A Thread B都已经正确 Attach。并且gCallback也是合法的 GlobalRef。那么Thread A ↓ CallVoidMethod(gCallback) Thread B ↓ CallVoidMethod(gCallback)从“引用能不能跨线程使用”这件事上说GlobalRef 是可以的。但这并不意味着你的整个程序已经线程安全。比如Thread A 正在 CallVoidMethod(gCallback) Thread B 同时 DeleteGlobalRef(gCallback)那照样可能出问题。这时候已经不是LocalRef / GlobalRef问题。而进入了Race Condition Mutex Atomic 串行化也就是我们后面专门要讲的并发安全问题。文档也明确区分了这两层GlobalRef 可以跨线程使用但共享状态、释放时机以及 Java 回调对象本身是否允许并发调用仍然需要单独考虑。JNI多线程_从线程本质到真正理解透十三、现在终于可以把 JNI 多线程拆成两条线了上一篇线程是谁 ↓ ART 认不认识 ↓ JavaVM Attach JNIEnv Detach这一篇Java 对象引用 还能不能继续使用 ↓ LocalRef GlobalRef所以JNI 多线程 ├─ 线程身份 │ ├─ JavaVM │ ├─ JNIEnv │ ├─ Attach │ └─ Detach │ └─ 对象生命周期 ├─ LocalRef ├─ NewGlobalRef └─ DeleteGlobalRef这两条线绝对不要混。十四、以后看到代码只问两个问题比如std::thread([callback] { // 以后调用 Java }).detach();不要马上背 API。先问第一问 现在是哪条线程 → 新的 Native Thread → ART 认不认识 → 不认识就 Attach再问第二问 callback 是什么引用 → native 方法参数 → 默认 LocalRef → 要异步长期使用 → NewGlobalRef答案自然就出来了。这比死记JNIEnv 不能跨线程 callback 要 NewGlobalRef稳定得多。十五、这一篇最后只记四句话第一句LocalRef / GlobalRef 解决的是 Java 对象引用生命周期不是线程 Attach。第二句JNI 方法参数里的 jobject 通常是 LocalRef只适合当前 JNI 调用生命周期内使用。第三句如果对象要跨当前 JNI 调用、异步以后继续使用就需要考虑 NewGlobalRef并在不用时 DeleteGlobalRef。第四句线程正确和对象引用正确是两件事Native Thread 可能既需要 Attach也需要 GlobalRef。所以 JNI 多线程到现在已经有了两问① 现在是哪条线程 ART 认不认识 ② 我要使用的 Java 对象引用 还能不能活到那个时候文档最后实际上也是把这两条线明确分开的。JNI多线程_从线程本质到真正理解透而下一篇我们要进入第三个问题假设线程也对了GlobalRef 也对了但突然有 3 条 Binder Thread 同时进来怎么办这就不再是 JNI 引用问题了。而是下一篇《Binder 线程池到底是什么为什么最后还是普通多线程问题》从这里开始我们会把Binder Thread Pool Race Condition 共享状态连起来。然后再下一篇专门讲Mutex Atomic 串行化三种并发处理方式到底分别什么时候用。