Android 開發中的線程模型和解決思路
在 Android 開發中,線程扮演着重要的角色,雖然隨着協程技術的普及,純應用層開發中,線程的使用已經不是問題,但是對於 JNI 開發,至少有一半的崩潰是線程相關問題引起的。所以本文將用實戰的方式,逐步向讀者揭示 JNI 開發中使用線程可能遇到的問題,並提供自己的解決思路。
一個概念的引入是爲了解決某些新問題,**「線程就是爲了解決多任務並行」**而引入的概念。但是引入新概念必定也會引入新的問題,線程引入的主要問題就是數據競爭,而 JNI 開發中卻不僅僅有數據競爭,讓我們逐步揭開線程的神祕面紗吧。
使用線程的三種場景
在之前的文章(NDK 開發概論)中,我簡單地把 JNI 開發分爲了三個部分:一部分是 Java 開發,一部分是 C++ 開發,然後通過 JNI 將兩部分連接起來。這三部分都會涉及到線程開發相關的問題。 前面也提到過線程間主要的問題是數據競爭,但是並不是所有場景都存在數據競爭。爲了對線程開發有比較全面的理解,我們可以通過對使用場景建模,來從簡單到複雜,對線程問題做個比較全面的概括。爲了方便闡述,下文中使用的主線程代表當前的執行環境,子線程代表通過主動創建線程得到的線程環境。
數據只在子線程中讀寫
線程的主要功能是執行並行任務,最簡單的任務就是給它個輸入,它根據輸入和執行代碼,得到執行結果,然後結束執行。這是線程中最簡單的模型,和傳統的單線程開發一樣,只多了個線程創建的內容。 所以這種場景下開發多線程應用也很簡單。如 Java 部分,我們使用 Kotlin 中的thread
構造器就能創建線程。構造器接收多個可選參數,但是必須有最後的參數,這個參數就是新線程的執行任務,當執行任務完成後,線程就自動退出,相關資源也會被系統回收。
fun simpleThread(){
thread(name = "SimpleThread") {
Log.i("TAG","task start")
Thread.sleep(1000)
Log.i("TAG","task end")
}
}
C++ 中,使用線程有兩種方式:一種是使用標準庫的std::thread
,另一種是使用 C 庫中的pthread
,不過通常爲了跨平臺方便,我是習慣使用 C++ 的標準庫。使用方式也和 Kotlin 差不多。
#include <string>
#include <thread>
#include <chrono>
#include <android/log.h>
void simpleThread(){
std::thread t{std::move([](){
__android_log_print(ANDROID_LOG_INFO,"TAG","Task start from jni");
std::this_thread::sleep_for(std::chrono::seconds(1));
__android_log_print(ANDROID_LOG_INFO,"TAG","Task end from jni");
})};
}
可以看到,在 C++ 中,創建一個新線程也很容易。不過,假如你運行上面的代碼,你會發現應用奔潰了:
Fatal signal 6 (SIGABRT), code -1 (SI_QUEUE)
std::terminate()
std::__ndk1::thread::~thread()
這是因爲線程在 simpleThread 函數退出時,線程對象t
的作用域也即將結束,需要銷燬線程對象。但是這時候t
執行的線程任務還沒有結束,不能銷燬。系統不知道你到底要結束線程,還是等待線程任務結束,所以拋出了錯誤。解決方法也很簡單,你可以使用t
的成員方法join
,等待線程任務結束,也可以使用t
的成員方法detach
,讓線程暫時保留直到執行完任務後自己銷燬。 這裏我們希望線程能在執行完任務後自己銷燬,所以添加以下代碼就可以解決這個問題
t.detach();
也許你會好奇,爲了 C++ 會比 Kotlin 多這個步驟呢?其實 Kotlin 也有相應的 API,只不過它是基於 JVM 的,它有自動回收功能,所以不需要我們做決定。 這個模型很簡單,我們除了能決定什麼時候啓動線程和執行的線程任務外,對線程狀態幾乎一無所知。很多時候我們可能希望對線程狀態有更多的感知能力,從而規劃新的任務。
使用回調通知狀態
一種成熟的方案是使用回調。回調是一種最簡單的解決方案,我們可以通過定義回調函數,在回調中傳遞線程狀態,而從讓主線程讀取到想要的結果。但是回調只是解決了狀態讀取的問題,並沒有轉換線程環境。所以這種情況只能適用於對線程不敏感的場景。
void simpleThread(std::function<void()> func){
std::thread t{[func=std::move(func)](){
__android_log_print(ANDROID_LOG_INFO,"TAG","Task start from jni");
std::this_thread::sleep_for(std::chrono::seconds(1));
__android_log_print(ANDROID_LOG_INFO,"TAG","Task end from jni");
func();
}};
t.detach();
}
上面的示例中,我增加了一個回調函數,並在子線程任務執行完後,執行回調函數。這樣我們就能在子任務結束後,知道任務執行的結果了。
14:11:29.714 11117 SimpleCPP top.deepthinking.blogsample I Task start
14:11:29.714 11117 SimpleCPP top.deepthinking.blogsample I Task end
14:11:29.717 11192 SimpleCPP top.deepthinking.blogsample I Task start from jni
14:11:29.718 11191 SimpleKotlin top.deepthinking.blogsample I task start
14:11:30.718 11192 SimpleCPP top.deepthinking.blogsample I Task end from jni
14:11:30.718 11192 SimpleCPP top.deepthinking.blogsample I Task callback
14:11:31.719 11191 SimpleKotlin top.deepthinking.blogsample I task end
數據在主子線程中不同時讀寫
在前面的模型中,我們通過線程,可以執行並行任務了,並且能使用回調,知道並行任務的狀態。但是也引入了新問題——任務狀態執行在子線程。這個問題對於 UI 應用開發是很致命的。因爲很多線程狀態是用來改變 UI 的,現在線程狀態執行在子線程,無法直接改變 UI,只能通過間接方式實現,這樣增加了複雜度。所以回調方案並不是一個好方案。 於是我們有了 Future。Future 代表一個異步操作,可以在主線程獲取到操作結果。也就是說它相比於回調,不僅可以獲取到操作結果,還可以轉換線程環境。它是一種成熟的異步轉同步方案,所以 Kotlin 和 C++ 都提供了 Future。 在 Kotlin 中,Future 通常和線程池一塊使用——通過向線程池提交任務,提交接口會返回一個 Future 對象,通過 Future 對象可以獲取到異步操作的結果。
private fun futureThread(): Future<Boolean> {
val executor = Executors.newSingleThreadExecutor()
return executor.submit(Callable<Boolean> {
Log.i(FUTURE_TAG, "task start")
Thread.sleep(2000)
Log.i(FUTURE_TAG, "task end")
true
})
}
在 C++ 中,使用 Future 需要導入future
文件頭,裏面主要提供兩個類四個步驟來完成異步轉同步操作。 首先創建一個std::promise<T>
變量,這個範型參數代表線程任務的返回值。
std::promise<bool> promise;
第二步,創建一個子線程,並將std::promise<T>
變量作爲參數傳遞到子線程中。這裏有個關鍵點,std::promise<T>
變量只能通過引用或者指針傳遞,也就是兩個線程使用的必須是同一個對象。
void futureThread(std::promise<bool>& promise){
std::thread t{[&promise](){
__android_log_print(ANDROID_LOG_INFO,FUTURE_TAG,"Task start from jni");
std::this_thread::sleep_for(std::chrono::seconds(1));
__android_log_print(ANDROID_LOG_INFO,FUTURE_TAG,"Task end from jni");
promise.set_value_at_thread_exit(true);
}};
t.detach();
}
第三步,在主線程中通過std::promise<T>
變量的get_future()
方法獲取到std::future<T>
對象。
auto future=promise.get_future();
最後一步,啓動子線程,並且在主線程中通過std::future<T>
對象的get()
方法獲取線程任務的結果。
futureThread(promise);
auto value=future.get();
通過 Future,我們把任務狀態從子線程移動到主線程,但是又引入了新問題——主線程被阻塞了。
14:23:14.782 16246 FutureCPP top.deepthinking.blogsample I Task start
14:23:14.783 16365 FutureKotlin top.deepthinking.blogsample I task start
14:23:14.783 16366 FutureCPP top.deepthinking.blogsample I Task start from jni
14:23:15.784 16366 FutureCPP top.deepthinking.blogsample I Task end from jni
14:23:15.785 16246 FutureCPP top.deepthinking.blogsample I Task end with true
14:23:16.784 16365 FutureKotlin top.deepthinking.blogsample I task end
14:23:16.784 16246 FutureKotlin top.deepthinking.blogsample I Task result true
有沒有不阻塞主線程,又能將結果返回主線程的方法呢,答案是有,在 Java 端,我們可以使用 Handler 來實現。 在主線程中,我們先創建一個 Handler,然後在子線程中使用 Handler 向主線程發送消息。這樣我們就可以成功把線程任務結果返回主線程了。
private fun handler(){
Log.i(HANDLER_TAG, "Task start")
val handler= Handler(Looper.getMainLooper()) { msg ->
Log.i(HANDLER_TAG, "task end with ${msg.what}")
true
}
thread(name = "HandlerThread") {
Log.i(HANDLER_TAG, "task start")
Thread.sleep(2000)
Log.i(HANDLER_TAG, "task end")
handler.sendEmptyMessage(1)
}
}
於是我們得到了以下結果。
15:05:20.558 3033 HandlerKotlin top.deepthinking.blogsample I Task start
15:05:20.563 3086 HandlerKotlin top.deepthinking.blogsample I task start
15:05:22.564 3086 HandlerKotlin top.deepthinking.blogsample I task end
15:05:22.565 3033 HandlerKotlin top.deepthinking.blogsample I task end with 1
當然,如果你熟悉 Kotlin 的協程,那麼 Handler 就完全不需要了。協程不僅能解決線程切換的問題,還能輕鬆取消,獲取計算結果等。如果你對 Rx 情有獨鍾,那麼 Rx 也是一個不錯的選擇。 雖然協程很美好,但是有些場景卻不能滿足我們的要求。
數據在主子線程間同時讀寫
對於數據流,上面的這些方案已經能處理得很好了。但是在很多時候,我們需要多個線程協同工作。協同工作要求我們在多個線程間交換數據。數據不再像前面兩種情況一樣是單向的可預測的流向,而是每時每刻在不同的線程流轉。 還是從最簡單的情況開始討論——兩個線程修改同一個數據,讓我們來看下面這個簡單的例子:
private fun dataRace() {
var target = 0
thread {
for (i in0 until 10000) {
target+=1
Log.i(RACE_TAG,"t1 $target")
}
}
thread {
for (i in0 until 10000) {
target+=1
Log.i(RACE_TAG,"t2 $target")
}
}
}
運行這個程序,按照預期,最終打印的結果應該是 20000 對吧。但是實際上不是,可能是 20002,19994 或者 19989 等等。
22:14:41.001 12690 RaceKotlin top.deepthinking.blogsample I total = 20002
這種數據和預期不一致的現象叫作數據競爭。但是爲什麼會出現這種現象呢?我們知道線程是資源和調度的最小單位,線程被創建時都有屬於自己的資源,這些資源不與其他線程共享。當線程內訪問外部資源時,它總是通過私有資源訪問,操作系統再將更改同步到主存中。這是兩個步驟,操作系統在調度線程時可能在任意時刻暫停或者恢復線程。在某一時刻某一線程在執行完第一步後,時間片被另一個線程搶走了,恰巧這個線程也在操作這個數據,它的操作順利執行完了。最終造成的結果是,兩個線程都更新了數據,但是最終的數據只更新了一次。這還只是一種情況,在多種不確定因素的作用下,導致數據最終不符合預期。 知道了原因,解決方案也就很清晰了:要麼讓操作變成一步,要麼讓兩步的操作是不可打斷的,把兩個方案糅合成一整招就是原子操作。C++ 和 Kotlin 都提供了原子操作的解決方案。
原子操作
由於原子操作接口都差不多,還是用 Kotlin 來演示,C++ 的只需要多個引入atomic
文件頭的步驟就可以了。我們直接在上面的例子的基礎上修改,修改方式也很簡單,直接把Int
換成AtomicInteger
,並把相加改爲incrementAndGet()
即可。
private fun dataRace() {
val target = AtomicInteger(0)
thread {
for (i in0 until 10000) {
target.incrementAndGet()
Log.i(RACE_TAG,"t1 $target")
}
}
thread {
for (i in0 until 10000) {
target.incrementAndGet()
Log.i(RACE_TAG,"t2 $target")
}
}
}
使用原子操作修改代碼後,我們如願以償得到了 20000,但是它雖然解決了一致性問題,但是卻不能做得更多了。 在線程協作中,數據往往是多樣的,並且相互依賴的,一個數據更新的同時,另一個數據也必須更新,這種更新鏈條也是不可打斷的,不然同樣會導致最終的結果不可預測。原子操作只是解決了單數據的一致性問題,而解決這種多數據之間的一致性問題需要新的思路和工具。 這次我們用 C++ 來演示一下這種現象:
void lock(){
staticstd::atomic<int> counter{0};
staticstd::atomic<int> task{0};
auto countTask{[&](int id){
for(int i=0;i<10000;++i){
++counter;
std::this_thread::yield();
++task;
if(counter.load()!=task.load()){
__android_log_print(ANDROID_LOG_INFO,LOCK_TAG,"Thread %d,counter %d,task %d",id,counter.load(),task.load());
}
}
}};
std::thread t1{countTask,1};
std::thread t2{countTask,2};
t1.detach();
t2.detach();
}
代碼中有兩個關鍵點,一個是兩個std::atomic
變量都是靜態的,這保證函數執行完後,變量在線程執行中是有效的。另一個是std::this_thread::yield()
,在更新兩個數據的中間,我用這個方法讓出了時間片,模擬數據更新時被打斷的場景。這段代碼執行的部分結果如下。
09:32:07.477 25383 LockCPP top.deepthinking.blogsample I Thread 2,counter 18650,task 18649
09:32:07.477 25382 LockCPP top.deepthinking.blogsample I Thread 1,counter 18651,task 18650
09:32:07.477 25383 LockCPP top.deepthinking.blogsample I Thread 2,counter 18652,task 18651
09:32:07.477 25382 LockCPP top.deepthinking.blogsample I Thread 1,counter 18653,task 18652
09:32:07.477 25383 LockCPP top.deepthinking.blogsample I Thread 2,counter 18654,task 18653
回到最基本的模型,爲什麼單線程時不存在這些問題呢?因爲單線程模型中,即使線程被掛起了,下一次恢復的時候,數據還是之前的狀態。而在多線程中,某個線程被掛起後,其他線程也會執行修改操作,打亂原本一致的數據。所以我們發現,要想保證數據的一致性,就需要保證這些數據被同時修改。針對這種情況,前輩們推出了鎖的解決方案。
鎖
顧名思義,鎖的概念和用法都和現實中一樣,只是代碼裏的鎖是用來鎖定資源的。鎖就好比是火車上的衛生間的門,排隊去衛生間的人就好比是訪問資源的線程。在衛生間沒人的時候,門是開着的,去衛生間的人就可以直接使用衛生間。一旦人進入衛生間後,把門鎖上了,衛生間就和外部隔離開了。此時外面想再去衛生間的人就只能不斷查看衛生間的狀態,並且在上一個使用衛生間的人離開後才能使用衛生間。而在衛生間的人,就獨佔了衛生間這個資源,只要他沒有離開,外面的人就進不去。鎖的出現解決了多線程訪問同一個資源的問題,能保證資源訪問的原子性。 和它概念一樣,在使用它也和現實中使用鎖是一樣的步驟,總共分三步
-
獲取鎖
-
使用鎖
-
釋放鎖 我們把上面的例子稍微添加點代碼,就能實現資源的一致性修改。
void lock(){
staticstd::atomic<int> counter{0};
staticstd::atomic<int> task{0};
staticstd::mutex mutex;
auto countTask{[&](int id){
for(int i=0;i<10000;++i){
std::lock_guard<std::mutex> lock{mutex};
++counter;
std::this_thread::yield();
++task;
if(counter.load()!=task.load()){
__android_log_print(ANDROID_LOG_INFO,LOCK_TAG,"Thread %d,counter %d,task %d",id,counter.load(),task.load());
}
}
}};
std::thread t1{countTask,1};
std::thread t2{countTask,2};
t1.detach();
t2.detach();
}
代碼中增加了一個std::mutex
,這個就是鎖了,它提供了獲取鎖和釋放鎖的接口。某個線程只要一旦通過它的lock
獲取到鎖後,資源就一直會被它佔有着,直到它自己釋放鎖。示例中並沒有手動調用lock
,unlock
,因爲std::lock_guard
會在創建對象時自動調用lock
,出了對象的作用域後,對象被銷燬,就自動調用unlock
,所以不需要手動調用。 經過這個修改後,我們發現,就不會出現異常的結果輸出了,成功保護了數據鏈的一致性。 線程間不僅有競爭也有合作,前面的例子都在描述競爭的情況,但是合作也是不可或缺的部分,其合作的方式就是線程間通信。
線程間通信
我們知道線程的運行狀態是不可知的,先開始運行的線程不一定先完成任務,後運行的線程也不一定後完成。所以在兩個線程一起完成同一個任務時,不能假設任務在不同線程的運行狀態,而是需要數據同步。數據同步可以使用前面介紹的原子操作或者鎖。不過爲了及時知道新的狀態,原本空閒的線程就要不斷輪詢,這樣做的代價就是佔用了極高的 CPU 資源。雖然在某些場景可以通過實驗找到合適的輪訓間隔,但是大部分場景是不現實的。爲了解決這種問題,又引入了條件變量。 條件變量有兩種操作:等待和喚醒。首先使用鎖鎖定共享的資源,執行數據更新,更新後放棄鎖資源,使用條件變量的notify_one
或者notify_all
通知其他的等待線程。在另一個線程,同樣也是需要先獲取鎖,因爲我們要使用共享的資源,然後調用條件變量的wait
等待資源更新結束。wait
等待時會放棄鎖資源,直到被其他線程喚醒,喚醒的線程會重新獲取到鎖,並接着往下執行。
void cond() {
staticint counter{0};
staticstd::condition_variable con;
staticstd::mutex mutex;
staticbool used = false;
std::thread t1{[]() {
for (int i = 0; i < 10; ++i) {
{
std::unique_lock lk{mutex};
con.wait(lk, []() { return !used; });
__android_log_print(ANDROID_LOG_INFO, COND_TAG, "Use counter %d ", counter);
used = true;
}
con.notify_one();
}
}};
std::thread t2{[]() {
for (int i = 0; i < 10; ++i) {
{
std::lock_guard<std::mutex> lk{mutex};
++counter;
used = false;
__android_log_print(ANDROID_LOG_INFO, COND_TAG, "Create counter %d ", counter);
}
con.notify_one();
std::unique_lock lk{mutex};
con.wait(lk, []() { return used; });
}
}};
t1.detach();
t2.detach();
}
代碼演示的是生產者 - 消費者模型,生產者線程 t2 先創建數據,然後通知消費者 t1 使用,自己則進入等待狀態。消費者 t1 默認進入等待狀態,一直等到 t2 通知它數據準備好了。它消費完數據後,接着通知 t2 繼續生產數據。兩個線程就這樣相互配合一起完成工作。它們工作的部分結果如下
23:19:22.120 28463 CondCPP top.deepthinking.blogsample I Create counter 7
23:19:22.120 28462 CondCPP top.deepthinking.blogsample I Use counter 7
23:19:22.120 28463 CondCPP top.deepthinking.blogsample I Create counter 8
23:19:22.120 28462 CondCPP top.deepthinking.blogsample I Use counter 8
23:19:22.120 28463 CondCPP top.deepthinking.blogsample I Create counter 9
23:19:22.120 28462 CondCPP top.deepthinking.blogsample I Use counter 9
23:19:22.120 28463 CondCPP top.deepthinking.blogsample I Create counter 10
23:19:22.120 28462 CondCPP top.deepthinking.blogsample I Use counter 10
條件變量的關鍵在於理解等待時釋放鎖,喚醒後重新獲得鎖,只要設置好等待條件,配合鎖的使用,就能實現線程間的通信。
JNI 中的線程
在處理 JNI 時,問題更加棘手。我們來看個最簡單的例子,把 JNI 參數傳遞到線程中處理。 首先看看 JNI 參數
extern "C" JNIEXPORT void JNICALL
Java_top_deepthinking_jnithread_NativeLib_execute(
JNIEnv *env,
jobject obj) {
auto cls=env->GetObjectClass(obj);
auto id=env->GetMethodID(cls,"test","()V");
jni(env,obj,id);
}
就是準備調用參數,然後調用jni
函數
void jni(JNIEnv* env,jobject obj,jmethodID id){
std::thread t{[env,obj,id](){
__android_log_print(ANDROID_LOG_INFO, JNI_TAG, "In thread");
env->CallVoidMethod(obj,id);
}};
t.detach();
}
在子線程中直接調用 Java 端方法。看着是不是沒有問題,而實際上運行這段代碼後,應用會奔潰,並輸出錯誤:java_vm_ext.cc:591] JNI DETECTED ERROR IN APPLICATION: a thread (tid 7521 is making JNI calls without being attached
。 這個錯誤是因爲JNIEnv
是線程私有的,只要線程需要調用 JNI 函數,它就必須正確初始化自己的JNIEnv
。怎麼初始化呢?通過JavaVM
的AttachCurrentThread
,而JavaVM
又需要通過實現JNI_OnLoad(JavaVM *vm, void *reserved)
來獲取。
static JavaVM* g_vm= nullptr;
extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved){
g_vm=vm;
return JNI_VERSION_1_6;
}
得到了JavaVM
後就能在線程中初始化JNIEnv
了,所以我們把jni
函數修改爲如下。
void jni(jobject obj,jmethodID id){
std::thread t{[obj,id](){
JNIEnv* env= nullptr;
auto res=g_vm->AttachCurrentThread(&env, nullptr);
if(JNI_OK!=res){
return;
}
__android_log_print(ANDROID_LOG_INFO, JNI_TAG, "In thread");
env->CallVoidMethod(obj,id);
g_vm->DetachCurrentThread();
}};
t.detach();
}
一旦JNIEnv
初始化成功後,在線程結束前,必須調用DetachCurrentThread
分離 JNI 環境,以便清理資源。通過這次修改後,應用能成功運行嗎?答案是還不行,這次是新的崩潰:java_vm_ext.cc:591] JNI DETECTED ERROR IN APPLICATION: JNI ERROR (app bug): jobject is an invalid JNI transition frame reference: 0x7fe31f1428 (use of invalid jobject)
。這個錯誤是因爲 JNI 參數都是局部的,在 JNI 調用結束後,這些局部變量就失效了,顯然,obj
在子線程使用時,JNI 調用早就結束了,所以應用崩潰。 解決方法也很簡單,既然參數是局部的,那麼把它升級爲全局變量就好了,而剛好JNIEnv
有NewGlobalRef
來實現這個目的。 所以,接着修改jni
函數爲如下
void jni(JNIEnv* env,jobject obj,jmethodID id){
std::thread t{[obj=env->NewGlobalRef(obj),id](){
JNIEnv* env= nullptr;
auto res=g_vm->AttachCurrentThread(&env, nullptr);
if(JNI_OK!=res){
return;
}
__android_log_print(ANDROID_LOG_INFO, JNI_TAG, "In thread");
env->CallVoidMethod(obj,id);
env->DeleteGlobalRef(obj);
g_vm->DetachCurrentThread();
}};
t.detach();
}
NewGlobalRef
把obj
升級爲全局變量,這樣在子線程中,obj
就可以一直使用。不過和JNIEnv
一樣,在對象不再使用後,必須調用DeleteGlobalRef
刪除全局變量,以便清理資源。 這次修改後,應用能成功運行了
23:28:22.345 5236 JniCPP top.deepthinking.blogsample I In thread
23:28:22.347 5131 KotlinLib top.deepthinking.blogsample I test in kotlin
總結一下,JNI 線程的使用需要考慮以下幾點:
-
JNIEnv 是線程私有的,調用 JNI 函數時,需要使用
AttachCurrentThread
初始化JNIEnv
,並且在線程退出前調用DetachCurrentThread
分離 JNI 環境,以便清理資源; -
JNI 參數是局部的,涉及到異步調用,需要配合使用
NewGlobalRef
和DeleteGlobalRef
來升級爲全局變量,保證數據的有效性;
總結
線程是開發過程中重要且基礎的話題,可以通過數據的使用場景對它們建模。如果數據對線程不敏感,那麼回調就是簡單高效的處理方法。如果數據經過處理後需要回到當前線程,那麼 Handler 或者 Future 是個好選擇。處理數據流是 Kotlin 協程的優勢。最複雜的當屬多線程同時對數據進行操作,如果僅僅需要多線程共享某個數據,那原子操作就足夠了。如果需要對資源訪問或者某些不可中斷的操作進行限制,那麼鎖就是爲它設計的。多線程之間需要協調數據,則少不了條件變量。JNI 環境由於數據的特殊性,需要配合使用全局變量和 JNIEnv。 好了,這期的分享就到這裏,咱們青山不改,綠水長流,下期見。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/57TfDB-WwWMGpEWBKU0jkw