JNI 函数是线程安全的吗?
Posted
技术标签:
【中文标题】JNI 函数是线程安全的吗?【英文标题】:Are JNI functions thread-safe? 【发布时间】:2018-06-17 11:46:16 【问题描述】:我正在尝试使用 OpenMP 来并行化将 Java 字符串数组转换为 C 字符串数组 (char**) 的过程:
char** convert(JNIEnv *env, jobjectArray jstringArray, int n_threads)
int n_rows =(*env)->GetArrayLength(env, jstringArray);
char **cArray = (char **) malloc(n_rows * sizeof(char*));
int i=0;
jstring row;
if(n_threads <= 0 ) n_threads = 1;
#pragma omp parallel for num_threads(n_threads) private(i, row)
for (i=0; i<n_rows; i++)
row = (jstring) (*env)->GetObjectArrayElement(env, jstringArray, i);
cArray[i] = (char*)(*env)->GetStringUTFChars(env, row, 0);
printf("cArray[%d]: %s thread:%d row:%p env:%p jstringArray:%p\n", i, cArray[i], omp_get_thread_num(), row, env, jstringArray);
return cArray;
但是,在获取 jstring 行时,我似乎遇到了竞争条件:
n_threads = 1 的输出:
cArray[0]: AA thread:0 row:0x7f5b1c000c90 env:0x7f5b300091f8 jstringArray:0x7f5bb3ffdc38
cArray[1]: BB thread:0 row:0x7f5b1c000c98 env:0x7f5b300091f8 jstringArray:0x7f5bb3ffdc38
cArray[2]: CC thread:0 row:0x7f5b1c000ca0 env:0x7f5b300091f8 jstringArray:0x7f5bb3ffdc38
cArray[3]: DD thread:0 row:0x7f5b1c000ca8 env:0x7f5b300091f8 jstringArray:0x7f5bb3ffdc38
n_threads = 3 的输出:
cArray[0]: AA thread:0 row:0x7f434c004050 env:0x7f434800a9f8 jstringArray:0x7f435b8f0c48
cArray[1]: BB thread:0 row:0x7f434c004060 env:0x7f434800a9f8 jstringArray:0x7f435b8f0c48
cArray[2]: **CC** thread:1 **row:0x7f434c004058** env:0x7f434800a9f8 jstringArray:0x7f435b8f0c48
cArray[3]: **CC** thread:2 **row:0x7f434c004058** env:0x7f434800a9f8 jstringArray:0x7f435b8f0c48
冲突函数似乎是 GetObjectArrayElement(),它为两个不同的线程(示例中的 1 和 2)和 2 个不同的数组索引(2 和 3)返回相同的引用。
这种行为是上述 JNI 函数固有的,还是我遗漏了什么?
【问题讨论】:
在私有列表中声明循环变量 i 看起来很可疑。 @AlexF 我知道它默认是私有的 您只能在已附加到 JVM 的线程上进行 JNI 调用。 @AlexCohn 这实际上很有帮助。谢谢。 @AlexCohn JRE 8。我已经通过将每个线程附加到 JVM 来修复它。 【参考方案1】:一种可能的解决方案是在访问 JNI 函数之前将每个线程附加到 JVM,如下所示:
JavaVM* jvm = NULL;
JNIEnv *t_env;
(*env)->GetJavaVM(env, &jvm);
#pragma omp parallel num_threads(n_threads) private(t_env)
(*jvm)->AttachCurrentThread(jvm, (void**)&t_env, NULL);
// Call JNI functions here using t_env...
(*jvm)->DetachCurrentThread(jvm);
【讨论】:
附加/分离相当昂贵,我不建议在紧密循环中调用它们。问题在于,使用 OpenMP,您无法控制底层线程的创建和销毁。也许,这意味着应该为 JNI 使用一些其他更明确的多线程框架。 @AlexCohn 我正在定义一个并行区域,而不是一个循环。那么,每个线程不应该按照这个sn-p只调用一次AttachCurrentThread()和DetachCurrentThread()吗? @cppstudy 我定义的是并行区域,而不是循环。#pragma omp parallel for ...
紧跟for (...)
定义循环。
@AndrewHenle 我指的是我在这个答案上发布的 sn-p,这是亚历克斯发表评论的地方。正如您在答案中指定的那样,for 循环将作为 AttachCurrentThread() 和 DetachCurrentThread() 调用之间的#pragma omp。
@cppstudy 哦。我以为你指的是问题中的原始代码。【参考方案2】:
您可以拆分 #pragma omp parallel ...
和 #pragma omp for ...
指令以并行执行操作,但不能循环执行。类似问题请参见Execute piece of code once per thread in OpenMP without default constructor。
这样的东西应该可以工作(未经测试,所以你可能需要重新编码,因为我怀疑 private
指令的位置可能不正确):
char** convert(JNIEnv *env, jobjectArray jstringArray, int n_threads)
int n_rows =(*env)->GetArrayLength(env, jstringArray);
char **cArray = (char **) malloc(n_rows * sizeof(char*));
int i=0;
jstring row;
JavaVM* jvm = NULL;
JNIEnv *thr_env;
( *env )->GetJavaVM( env, &jvm );
if(n_threads <= 0 ) n_threads = 1;
#pragma omp parallel num_threads(n_threads) private( thr_env )
// attach this thread to the JVM
( *jvm )->AttachCurrentThread( jvm, ( void** ) &thr_env, NULL );
// run the for loop
#pragma omp for private(i, row)
for (i=0; i<n_rows; i++)
row = (jstring)
(*env)->GetObjectArrayElement(env, jstringArray, i);
cArray[i] = (char*)(*env)->GetStringUTFChars(env, row, 0);
printf(
"cArray[%d]: %s thread:%d row:%p env:%p jstringArray:%p\n",
i, cArray[i], omp_get_thread_num(), row, env, jstringArray);
( *jvm )->DetachCurrentThread( jvm );
return cArray;
【讨论】:
我已经尝试过与我之前发布的类似的东西,它确实有效。谢谢【参考方案3】:我以前从未使用过 JNI,但在 Google 上搜索“JNI 线程安全”时会返回 this 作为它的第一击:
JNI 接口指针(JNIEnv *)只在当前线程中有效。您不得将接口指针从一个线程传递到另一个线程,或缓存接口指针并在多个线程中使用它。 Java 虚拟机将在同一线程的本地方法的连续调用中传递相同的接口指针,但不同的线程将不同的接口指针传递给本地方法。
希望对您有所帮助。
【讨论】:
以上是关于JNI 函数是线程安全的吗?的主要内容,如果未能解决你的问题,请参考以下文章