0%

JNI函数调用之线程安全问题

JNI 开发是什么

因本人以前工作中做过此类工作,再加上老同事的疑问,所以,今天抽出时间来探讨一下JNI的开发问题和特定细节(调用的线程安全问题),其实这种JNI层调用和任何其他语言调用C/C++层结构都差不多,比如说CGO,其涉及到Golang和C/C++层的调用,很多方面都存在类似之处。

JNI开发是使用Java提供的本地化接口,比如C/C++(也可以是其他语言),允许Java虚拟机里面的已经编译的代码和外界的本地化代码进行交互。

因为Java是推崇平台可移植性的,本地化代码看起来破坏了可移植性,但是很多时候却是不可避免的,因为很多时候Java是无法实现一些比较底层功能的。

JNI开发需要哪些工具

在我的机器里面是安装的OpenJDK11和GCC11,运行平台是Linux环境,为了提高编码效率,我使用的是VSCode,外加Java插件,CMake插件和C++插件。

这里列举一下工具链:

  • OpenJDK 11
  • GCC 11
  • CMake 3.21
  • VSCode 最新版

截图:

vscode

关于OpenJDK为何找不到javah,可以参考这里,意思就是javah已经被移除了,现在可以通过javac来完成同样的操作:

1
javac -cp . -h abc MyCls.java

以上假设在当前目录的MyCls.java存在本地调用,它会在当前目录下创建一个abc目录,并且写入C++层的JNI调用头文件,你只需要在写一个和之对应的C++源代码即可。

术语解释

其实线程安全是什么意思都可以通过搜索找到,也许你已经知道什么意思,不过这里不妨碍我再叙述一遍:

线程安全是指某个函数在多线程的环境下被多次调用时,能够使得多线程的每个调用者都可以得到自己想要的正确结果。

主要因素

产生线程安全问题的原因是因为函数调用需要对公共变量进行修改

这会涉及到四种情形:

  • 静态Java本地调用改C++层的公共数据
  • 静态Java本地调用改Java层的公共数据
  • 动态Java本地调用改C++层的公共数据
  • 动态Java本地调用改Java层的公共数据

所以说只要涉及到公共数据都会产生线程安全的问题。

至于什么是静态/动态Java本地调用?区别就是对应本地调用是否被static修饰,修饰者属于类的调用,否则属于被申请的对象的调用,与之分别对应静态/动态调用。

还有C++/Java层公共数据是什么?C++层的公共数据就是C++里面全局可以访问的变量,而这里的Java层公共变量指的是用C++访问/修改在Java类或者Java对象中的公共变量。

实例操作

以下我写了一个demo用于阐述JNI调用的线程安全的问题。

Java部分

java部分的代码写得很简单,声明了几个本地JNI接口,并在main中调用之:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class MyCls {
public static void main(String[] args) {
if (loadLibrary()) {
sayHello();

ConSt st = new ConSt();

threadSafe(st); // 只要传进的st唯一即可达到线程安全的要求

System.out.println(mySafeInt);
System.out.println(st.mInt);

System.out.printf("unsafe int:%d\n", threadUnsafe());

MyCls obj = new MyCls();
obj.threadUnsafe2();
System.out.printf("unsafe int:%d\n", obj.myUnsafeInt);
}
}

// 加载c动态库
private static boolean loadLibrary() {
try {
System.loadLibrary("abc");
} catch(SecurityException e) {
e.printStackTrace();
return false;
} catch(UnsatisfiedLinkError e) {
e.printStackTrace();
return false;
}
return true;
}

private static long mySafeInt = 0;
private long myUnsafeInt = 0;

// 本地化接口
private static native void threadSafe(ConSt st);
private static native int threadUnsafe(); // 使用了C++层的公共变量,且没有加锁机制
private native void threadUnsafe2(); // 使用了Java层的公共变量,且没有加锁机制
}

上面的主体部分就是这三个本地调用的使用,threadSafe()实际上可以拆开成两个函数的,注意下面的C++部分的代码,为了省事,我把写在一块了。

threadSafe()是线程安全的在任意调用次数后,都会返回正确的结果,而下面的threadUnsafe()threadUnsafe2()则不是线程安全的,在很多线程执行时会得到混乱的结果。

C/C++部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
std::mutex mu_thread_safe;

/*
* Class: MyCls
* Method: threadSafe
* Signature: (LConSt;)V
*/
JNIEXPORT void JNICALL Java_MyCls_threadSafe
(JNIEnv *env, jclass clz, jobject cons_st) {
// -- 1 -- 使用C++的互斥锁来阻塞操作,这样可以保证线程安全
jfieldID _fieldId = env->GetStaticFieldID(clz, "mySafeInt", "J"); // 获得类中的静态成员变量

mu_thread_safe.lock();
env->SetStaticLongField(clz, _fieldId, 12345); // 这里通过互斥锁来达到线程安全
mu_thread_safe.unlock();

// -- 2 -- 通过形式参数形式传递变量进入,只要保证形参不同即可线程安全
jclass _cs_clz = env->FindClass("ConSt");
assert(_cs_clz != nullptr);
jfieldID _cs_fieldId = env->GetFieldID(_cs_clz, "mInt", "J");
env->SetLongField(cons_st, _cs_fieldId, 12345);
}

/*
* Class: MyCls
* Method: threadUnsafe
* Signature: ()V
*/
JNIEXPORT jint JNICALL Java_MyCls_threadUnsafe
(JNIEnv *, jclass) {
// -- 3 -- 在C++层存储公共数据,并进行改写不加锁,不是线程安全的
static int _unsafe_int = 0;
_unsafe_int++; // 这里不加任何互斥锁机制
return _unsafe_int;
}

/*
* Class: MyCls
* Method: threadUnsafe2
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_MyCls_threadUnsafe2
(JNIEnv *env, jobject obj) {
// -- 4 -- 或者C++层操作的公共数据是在java层的变量,亦不加锁,也不是线程安全的
jclass _clz = env->FindClass("MyCls");
assert(_clz != nullptr);
jfieldID fieldId = env->GetFieldID(_clz, "myUnsafeInt", "J");
assert(fieldId != nullptr);
jlong myUnsafeInt = env->GetLongField(obj, fieldId) + 1;
env->SetLongField(obj, fieldId, myUnsafeInt);
}

以上四处标识中,第一个函数threadSafe()处,通过C++的互斥锁来对公共变量修改是一种方案,但是它对于大量并发的操作而言,效率低下,因为它是互斥线性化的,所以一般推荐第二处标识的方案,它让用户传递一个自定义的类型对象,C++层就对这个对象进行修改,调用者负责它传递的对象唯一,那么对于大量的并发操作而言,得到的结果也必然是正确的。

对于第三处和第四处是线程安全要求下不能满足的反例,第三处是没有对C++层公共数据添加加锁机制,而第四处是和第三处类似的,不同之处在于它的修改是在Java代码里的变量。

关于GetFiledID()的第四个参数sign是什么意思?它是Java代码对这个函数的表述,可以参照下面这张图(还是从别人的那里截图过来的),更多详情可以查找官方文档。

sign

总结

保证线程安全的要求是对公共资源恰当使用,最好不要用公共资源,让调用者传递参数值作为修改变量来使用,效果最佳。

链接: demo下载

欢迎关注我的其它发布渠道