上一篇文章http://www.bkjia.com/kf/201202/119073.html說到 JNIEnv 是一個與線程相關的變數,即線程A有一個 JNIEnv變數, 線程B也有一個JNIEnv變數,由於線程相關,所以A線程不能使用B線程的 JNIEnv 結構體變數。
問題描述:
一個java對象通過JNI調用DLL中一個send()函數向伺服器發送訊息,不等伺服器訊息到來就立即返回,同時把JNI介面的指標JNIEnv *env(虛擬機器環境指標),和jobject obj儲存在DLL中的變數裡.一段時間後,DLL中的訊息接收線程接收到伺服器發來的訊息,並試圖通過儲存過的env和obj來調用先前的java對象的方法(相當於JAVA回調方法)來處理此訊息此時程式會突然退出(崩潰).
即前台JAVA線程發送訊息,後台線程處理訊息,歸屬於兩個不同的線程,不能使用相同的JNIEnv變數,這裡可以利用一個機制: 利用全域的 JavaVM * 指標得到當前線程的 JNIEnv* 指標,與在C++中兩個線程使用TLS進行局部儲存類似的原理。
具體方法:
擷取全域的JavaVM變數:
/* Our VM */
JavaVM *g_vm;
env->GetJavaVM(&g_vm); //來擷取JavaVM指標.擷取了這個指標後,將該JavaVM儲存起來。
線程 JNIEnv 指標,線程中擷取 JNIEnv 方法:
JNIEnv *e;
JavaVMAttachArgs thread_args;
thread_args.name = "NFC Message Loop";
thread_args.version = nat->env_version;
thread_args.group = NULL;
g_vm->AttachCurrentThread(&e, &thread_args); //後面的參數可以傳空
while(1){
//...
}
g_vm->DetachCurrentThread(); //使用完成後
經過如此以後,JNIEnv 就可以由每個線程獨自使用了。
而如果我們需要回調JAVA方法,jobject 也不能在多個線程中共用,如此可以在多個線程中使用了:
gs_object=env->NewGlobalRef(obj);//建立一個全域變數
將傳入的obj(局部變數)儲存到gs_object中,從而其他線程可以使用這個gs_object(全域變數)來操縱這個java對象了
完整範例程式碼如下:
java代碼:Test.java:
[java]
import java.io.*;
class Test implements Runnable
{
public int value = 0;
static{ System.loadLibrary("Test");}
public native void setEnev();//本地方法
public static void main(String args[]) throws Exception
{
Test t = new Test();
<span style="color:#FF0000;"> t.setEnev(); //調用本地方法 </span>
while(true)
{
Thread.sleep(1000);
System.out.println(t.value);
}
}
}
import java.io.*;
class Test implements Runnable
{
public int value = 0;
static{ System.loadLibrary("Test");}
public native void setEnev();//本地方法
public static void main(String args[]) throws Exception
{
Test t = new Test();
<span style="color:#FF0000;"> t.setEnev(); //調用本地方法 </span>
while(true)
{
Thread.sleep(1000);
System.out.println(t.value);
}
}
}
JNI代碼 Test.cpp:
static JavaVM *gs_jvm=NULL;
static jobject gs_object=NULL;
static int gs_i=10;
JNIEXPORT void JNICALL Java_Test_setEnev(JNIEnv *env, jobject obj)
{
env->GetJavaVM(&gs_jvm); //儲存到全域變數中JVM
//直接賦值obj到DLL中的全域變數是不行的,應該調用以下函數:
gs_object=env->NewGlobalRef(obj);
HANDLE ht=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)ThreadFun,0,NULL,NULL);
}
void WINAPI ThreadFun(PVOID argv)//JNI中線程回調這個方法
{
JNIEnv *env;
gs_jvm->AttachCurrentThread((void **)&env, NULL);
jclass cls = env->GetObjectClass(gs_object); //擷取JAVA線程中的全域對象
jfieldID fieldPtr = env->GetFieldID(cls,"value","I"); // 擷取JAVA對象
while(1)
{
Sleep(100);
//這裡改變JAVA對象的屬性值(回調JAVA)
env->SetIntField(gs_object,fieldPtr,(jint)gs_i++);
}
}
對於如上的思路,只要你理解了TLS的用法就很容易理解以上內容了。
附加介紹 TLS (thread-local storage) 一下,網上摘抄的內容:
線程是執行的單元,同一個進程內的多個線程共用了進程的地址空間,線程一般有自己的棧,但是如果想要實現某個全域變數在不同的線程之間取不同的值,而且不受影響。一種辦法是採用線程的同步機制,如對這個變數的讀寫之處加臨界區或者互斥量,但是這是以犧牲效率為代價的,能不能不加鎖呢?線程局部儲存就是幹這個的。
Windows中是根據線程局部儲存索引來標識的(這個標識的分配和釋放由TlsAlloc和TlsFree完成),有了個這個”標識“就可以在各個線程中調用TlsGetValue或者TlsSetValue讀取或者設定各線程各自的值;
DWORD TlsAlloc(void);
BOOL TlsFree(DWORD dwTlsIndex);
LPVOID TlsGetValue(DWORD dwTlsIndex);
BOOL TlsSetValue(DWORD dwTlsIndex, LPVOID lpTlsValue);
linux 平台對應的介面函數:
int pthread_key_create(pthread_key_t * key, void (*)(void *));
int pthread_key_delete(pthread_key_t);
void *pthread_getspecific(pthread_key_t);
int pthread_setspecific(pthread_key_t, const void *);
摘自 andyhuabing的專欄