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 函数是线程安全的吗?的主要内容,如果未能解决你的问题,请参考以下文章

我可以假设 Delphi NOW 函数是线程安全的吗?

静态函数范围对象的构造是线程安全的吗?

+= 运算符在 Python 中是线程安全的吗?

原子增加和比较是线程安全的吗

C/C++线程安全型队列的实现

java是线程安全的吗