本文章主要讲述的技巧为:Java层保存Native层中复杂数据的指针地址,Native需要时根据Java传递下来的地址重新强转回指针,以方便支持Java多线程并发创建多个对象进行调用。(即让每个对象能够保存属于自己的一份Native层数据"索引",必要时交由Native层去通过"索引"拿到数据进行处理)
一、场景介绍
在JNI开发过程有这样的一个场景:
用JNI封装调用另一个第三方算法so库,该SO库是能够支持多线程并发创建多个处理实例,同时处理数据的。则JNI在封装接口时,也需要注意接口方法并发的情况,这个时候必须尽可能使用局部变量,以免全局变量在某一线程中被修改,导致在另一线程不能按预期执行。
首先看下第三方so库的两个接口方法:
/**
* 创建对应token的算法处理实例,返回长度为inst_num的void型指针数组insts。
* 此方法需支持并发创建不同token的实例
*/
int create_instances(char* token, void **insts, size_t inst_num);
/**
* 使用insts数组中某一个inst实例去处理数据
* 此方法同样支持并发
*/
int process_data(void *inst, const int16_t *data, int data_size);
从以上两个方法可以看出,第一个方法create_instances调用生成的insts指针数组,需要在执行第二个方法process_data用到,通过传入该数组的某个实例元素给第一个参数inst,以完成方法的调用。
二、单线程封装的情况
我们先来看下单线程下的简单情况处理,一般会想到使用一个全局变量来保存insts指针数组,在create_instances创建实例时为全局变量insts赋值,在process_data时直接拿到全局变量insts使用,如下:
// 这里定义全局变量
void ** insts;
int inst_count;
extern "C"
JNIEXPORT jint JNICALL
Java_com_test_Sample_native_1create_1instances(JNIEnv *env, jobject thiz, jstring token, const jint inst_num) {
...
inst_count = inst_num;
insts = static_cast<void **>(malloc(sizeof(void *) * inst_num));
int create_ret = create_instances(token_chars, tokenLen, insts, inst_num);
...
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_test_Sample_native_1process_1data(JNIEnv *env, jobject thiz, jint inst_seq, jbyteArray data, jint size) {
...
jbyte *data_bytes = env->GetByteArrayElements(data, &isCopy);
int16_t *data_int16 = (int16_t *) data_bytes;
// 这里直接拿到全局变量insts进行使用
int ret = process_data(insts[inst_seq], data_int16, size);
env->ReleaseByteArrayElements(data, data_bytes, JNI_ABORT);
return ret;
}
对应Java的native接口定义:
public class Sample {
public native int native_create_instances(String token, int inst_num);
public native int native_process_data(int inst_seq, byte[] data, int size);
}
三、多线程封装的情况
在多线程的情况下,上面的封装形式就没法支持并发,尽管不同线程new了各自的Sample对象,但实际不同线程在Native层仍然是执行了同一个代码块,共享了一个全局变量insts(这里需要注意,C语言并不像Java可以new对象,产生出多一份的实例堆内存,包含全局变量引用)。l例如token1的线程创建实例,之后token2的线程也创建实例,此时insts所指向的是token2的实例,token1的线程拿token2的insts去处理就会出现对应不上,导致报错。
对于这种情况,我们可能很容易想到在C代码上维护一个加锁的容器,用来存放多个insts指针数组的,但这种方式用C语言实现起来较为麻烦,除了增加容器的增删查操作,还得注意加锁的正确性,而加锁也会导致降低一定的性能。
再进一步思考能否让Sample对象对应保存自己创建insts指针数组,在create_instances时将insts数组传递上去,调用process_data在传递下来,这样就不存在不同线程创建各自Sample对象会共享一个C全局变量insts的问题。但是insts指针数组不像基本数据类型可以直接简单地值传递,我们也不知道元素void型指针具体指向的地址存放怎样的数据格式,这种传递行不通。但换个思路,可以通过传递指针数组的地址给Sample保存,调用时传递地址下来C层,C层在对地址进行强制类型转换。经过请教及实践,发现是行得通的,具体实现如下:
Sample.java
public class Sample {
private long instAddress; // 由JNI层进行赋值
private int instNum;
private int createResult = -1;
private native int native_create_instances(String token, int instNum);
private native int native_process_data(long inst_adress, int inst_seq, byte[] data, int size);
public int createInstances(String token, int instNum) {
this.instNum = instNum;
// 这里调用返回成功后,Native层将给instAddress成员变量赋值
createResult = native_create_instances(token, instNum);
return createResult;
}
public int processData(int instSeq, byte[] data, int size) {
if (createResult == -1) {
System.out.println("error: create_instances ret != 0, please ensure instances is succeed before");
return -1;
}
System.out.println("instAddress=" + instAddress);
// 这里传递赋值过的成员变量instAddress,将指针数组地址传递给Native层
return native_process_data(instAddress, instCnt, instSeq, data, size);
}
}
主要改动:Sample类中新增了long型变量instAddress用以存储Native层传递的地址,在调用时数据处理时,并新增非Native方法processData将instAddress传递给本地native_process_data方法。
Native层:
// 全局引用保存Sample类class
jclass globalSampleClass;
extern "C"
JNIEXPORT jint JNICALL
Java_com_test_Sample_native_1create_1instances(JNIEnv *env, jobject thiz, jstring token, const jint inst_num) {
...
// 给局部变量insts指针数组分配内存
void **insts = static_cast<void **>(malloc(sizeof(void *) * inst_num));
int create_ret = create_instances(data_chars, data_len, insts, inst_num);
// 将insts数组指针地址保存到Java层的Sample对象的instAddress字段中,以便在其他函数使用
if (create_ret == 0) {
// 将insts指针数组的地址强制转换为long型数据
long inst_address = reinterpret_cast<long>(insts);
// 获得Sample的class引用
if (NULL == globalSampleClass) {
globalSampleClass = env->GetObjectClass(thiz);
}
// 获取Sample的成员变量instAddress的FiledID
jfieldID instAddressField = env->GetFieldID(globalSampleClass, "instAddress", "J");
// 给调用者Sample对象(thiz)的instAddress赋值
env->SetLongField(thiz, instAddressField, inst_address);
}
// 注意不能销毁局部变量insts指针数组所指向的内存,其他函数还要用到
...
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_test_Sample_native_1process_1data(JNIEnv *env, jobject thiz, jlong inst_address, jint inst_num, jint inst_seq, jbyteArray data, jint size) {
....
jbyte *data_bytes = env->GetByteArrayElements(data, &isCopy);
int16_t *data_int16 = (int16_t *) data_bytes;
// 将long型地址数据强制转换为void **进行使用
void **insts = (void **) inst_address;
int ret = process_data(insts[inst_seq], data_int16, size);
env->ReleaseByteArrayElements(data, data_bytes, JNI_ABORT);
return ret;
}
主要改动:
1. 去掉单线程情况下的两个全局变量,insts指针数组改为局部变量,将inst_count放到Java层存储,需要时传递下来;
2. 保存void ** 型指针数组地址到Java层的long型变量,之前对传递下来的long型地址强制转换回void **。
说明:局部变量insts所指向的内存最终会有另外一个销毁函数进行销毁,不是本文重点故忽略,实际开发需注意内存分配及释放,以免内存泄漏。
四、总结
关于JNI接口支持多线程的关键点:
1. Native层不使用全局变量,能简单改为局部变量的直接改局部变量;
2. Native必要的全局变量迁移到Java层进行保存,基础数据类型可以直接值传递,结构体数据或数组也可以采用传递地址的方式,将地址转换为long型数据传给Java对象,Native使用时再做强制转换。