JNI接口支持多线程并发调用的小技巧

本文章主要讲述的技巧为: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使用时再做强制转换。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值