程序员社区

JNI开发-线程操作

JNI开发-线程操作

线程操作

JNIEnv指针仅在创建它的线程有效。C/C++创建的线程默认是没有附加到JVM的,如果我们需要在本地线程线程访问JVM,那么必须先调用AttachCurrentThread将当前线程与JVM进行关联,然后才能获得JNIEnv对象。线程退出或不再需要使用JNIEnv时,我们必须通过调用DetachCurrentThread来解除连接,否则可能会导致线程不能正常退出或程序奔溃等问题。

函数 说明
AttachCurrentThread 将当前线程附件到JVM
DetachCurrentThread 解除当前线程与JVM的连接

Java中访问native方法并没有线程限制,所以我们的本地代码并不一定只会运行在main线程中 ,同时本地C/C++也可用创建子线程,在多线程的情况下,就不得不考虑 线程同步问题了。 Java中,JDK为我们提供了synchronized来处理多线程同步代码块 ,相应的在JNI中也提供了两个函数来完成线程同步。

函数 说明
MonitorEnter 进入临界区
MonitorExit 退出临界区

我们可以在 Native 代码中使用 POSIX 线程,就相当于使用一个库一样,首先需要包含这个库的头文件:

#include <pthread.h>

这个头文件中定义了很多和线程相关的函数,这里就暂时使用到了其中部分内容。

创建线程

POSIX 创建线程的函数如下:

int pthread_create(
    pthread_t* __pthread_ptr, 
    pthread_attr_t const* __attr, 
    void* (*__start_routine)(void*), 
    void* arg);

它的参数对应如下:

  • __pthread_ptr 为指向 pthread_t 类型变量的指针,用它代表返回线程的句柄。
  • __attr 为指向 pthread_attr_t 结构的指针,可以通过该结构来指定新线程的一些属性,比如栈大小、调度优先级等,具体看 pthread_attr_t 结构的内容。如果没有特殊要求,可使用默认值,把该变量取值为 NULL 。
  • 第三个参数为该线程启动程序的函数指针,也就是线程启动时要执行的那个方法,类似于 Java Runnable 中的 run 方法,它的函数签名格式如下:
void* start_routine(void* args)

启动程序将线程参数看成 void 指针,返回 void 指针类型结果。

  • 第四个参数为线程启动程序的参数,也就是函数的参数,如果不需要传递参数,它可以为 NULL 。

pthread_create 函数如果执行成功了则返回 0 ,如果返回其他错误代码。

接下来,我们可以体验一下 pthread_create 方法创建线程。

void *printThreadHello(void *) {
    cout<<("hello thread");
    // 切记要有返回值
    return NULL;
}
JNIEXPORT void JNICALL Java_com_jni_thread_TestDemo_naitveThread
        (JNIEnv *, jobject) {
    pthread_t pid;
    pthread_create(&pid, nullptr, printThreadHello, nullptr);
    pthread_join(pid, nullptr);

}
public class TestDemo {

    static {
        System.load("/Volumes/CodeApp/SourceWork/CPlus_workspace/JNI-Demo-Lib/cmake-build-debug/thread/libnative-lib.dylib");
    }

    public native void naitveThread(); // Java层 调用 Native层 的函数,完成JNI线程
    public native void closeThread(); // 释放全局引用


    public native void nativeFun1();

    public native void nativeFun2(); // 2

    public static native void staticFun3(); // 3

    public static native void staticFun4();


    public static void main(String[] args) {

        TestDemo testDemo = new TestDemo();

        testDemo.naitveThread();

    }

}

RUN>

*******************?输出结果?*************************
hello thread
Process finished with exit code 0   

将线程附着在 Java 虚拟机上

在上面的线程启动函数中,只是简单的执行了打印 log 的操作,如果想要执行和 Java 相关的操作,比如从 JNI 调用 Java 的函数等等,那就需要用到 Java 虚拟机环境了,也就是用到 JNIEnv 指针,毕竟所有的调用函数都是以它开头的。

pthread_create 创建的线程是一个 C++ 中的线程,虚拟机并不能识别它们,为了和 Java 空间交互,需要先把 POSIX 线程附着到 Java 虚拟机上,然后就可以获得当前线程的 JNIEnv 指针,因为 JNIEnv 指针只是在当前线程中有效。

通过 AttachCurrentThread 方法可以将当前线程附着到 Java 虚拟机上,并且可以获得 JNIEnv 指针。

AttachCurrentThread 方法是由 JavaVM 指针调用的,它代表的是 Java 虚拟机接口指针,可以在 JNI_OnLoad 加载时来获得,通过全局变量保存起来

static JavaVM *gVm = NULL;
JNIEXPORT int JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env;
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
        return JNI_ERR;
    }
    gVm = vm;
    return JNI_VERSION_1_6;
}

当通过 AttachCurrentThread 方法将线程附着当 Java 虚拟机上后,还需要将该线程从 Java 虚拟机上分离,通过 DetachCurrentThread 方法,这两个方法是要同时使用的,否则会带来 BUG 。

Native 线程中运行的方法:

class MyContext {
public:
    JNIEnv *jniEnv = nullptr;  // 不能跨线程 ,会奔溃
    jobject instance = nullptr; // 不能跨线程 ,会奔溃
};


void *myThreadTaskAction(void *pVoid) { // 当前是异步线程
    // 这两个是必须要的
    // JNIEnv *env
    // jobject thiz   OK

    MyContext *myContext = static_cast<MyContext *>(pVoid);
    // TODO 解决方式 (安卓进程只有一个 JavaVM,是全局的,是可以跨越线程的)
    JNIEnv *jniEnv = nullptr; // 全新的JNIEnv  异步线程里面操作
    jint attachResult = ::gVm->AttachCurrentThread(reinterpret_cast<void **>(&jniEnv), nullptr); // 附加当前异步线程后,会得到一个全新的 env,此env相当于是子线程专用env
    if (attachResult != JNI_OK) {
        return 0; // 附加失败,返回了
    }

    // 1.拿到class
    jclass mainActivityClass = jniEnv->GetObjectClass(myContext->instance);

    // 2.拿到方法
    jmethodID updateActivityUI = jniEnv->GetMethodID(mainActivityClass, "updateActivityUI", "()V");

    // 3.调用
    jniEnv->CallVoidMethod(myContext->instance, updateActivityUI);

    ::gVm->DetachCurrentThread(); // 必须解除附加,否则报错

    cout << ("C++ 异步线程OK") << endl;

    return nullptr;
}


JNIEXPORT void JNICALL Java_com_jni_thread_TestDemo_naitveThread
        (JNIEnv *env, jobject job) {

    MyContext *myContext = new MyContext;
    myContext->jniEnv = env;
    // myContext->instance = job; // 默认是局部引用,会奔溃
    myContext->instance = env->NewGlobalRef(job); // 提升全局引用

    pthread_t pid;
    pthread_create(&pid, nullptr, myThreadTaskAction, myContext);
    pthread_join(pid, nullptr);

}
public class TestDemo {

    static {
        System.load("/Volumes/CodeApp/SourceWork/CPlus_workspace/JNI-Demo-Lib/cmake-build-debug/thread/libnative-lib.dylib");
    }

    public native void naitveThread(); // Java层 调用 Native层 的函数,完成JNI线程

    public native void closeThread(); // 释放全局引用


    public native void nativeFun1();

    public native void nativeFun2(); // 2

    public static native void staticFun3(); // 3

    public static native void staticFun4();

    public void updateActivityUI() {
        System.out.println("print thread name current thread name is " + Thread.currentThread().getName());
        try {
            TimeUnit.MILLISECONDS.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


    public static void main(String[] args) {

        TestDemo testDemo = new TestDemo();

        System.out.println("-----------------------");
        testDemo.naitveThread();

    }

}

RUN>

*******************?输出结果?*************************
JNI_OnLoad
-----------------------
print thread name current thread name is Thread-0
C++ 异步线程OK

下面来写测试不同线程之间JavaVMJNIEnv 是否相同

JNIEXPORT void JNICALL Java_com_jni_thread_TestDemo_nativeFun1
        (JNIEnv *env, jobject job)
{
    JavaVM * javaVm = nullptr;
    env->GetJavaVM(&javaVm);

    // 打印:当前函数env地址, 当前函数jvm地址, 当前函数job地址,  JNI_OnLoad的jvm地址
    printf("nativeFun1 当前函数env地址%p,  当前函数jvm地址:%p,  当前函数job地址:%p, JNI_OnLoad的jvm地址:%p\n", env, javaVm, job, ::gVm);
    cout<< endl;
}

JNIEXPORT void JNICALL Java_com_jni_thread_TestDemo_nativeFun2
        (JNIEnv *env, jobject job)
{
    JavaVM * javaVm = nullptr;
    env->GetJavaVM(&javaVm);

    // 打印:当前函数env地址, 当前函数jvm地址, 当前函数job地址,  JNI_OnLoad的jvm地址
    printf("nativeFun2 当前函数env地址%p,  当前函数jvm地址:%p,  当前函数job地址:%p, JNI_OnLoad的jvm地址:%p\n", env, javaVm, job, ::gVm);
    cout<< endl;
}

void * run(void *) { // native的子线程 env地址  和  Java的子线程env地址,一样吗  不一样的
    JNIEnv * newEnv = nullptr;
    ::gVm->AttachCurrentThread(reinterpret_cast<void **>(&newEnv), nullptr);
    // 打印:当前函数env地址, 当前函数jvm地址, 当前函数clazz地址,  JNI_OnLoad的jvm地址

    printf("run jvm地址:%p,  当前run函数的newEnv地址:%p \n", ::gVm, newEnv);
    cout<< endl;

    ::gVm->DetachCurrentThread();
    return nullptr;
}

JNIEXPORT void JNICALL Java_com_jni_thread_TestDemo_staticFun3
        (JNIEnv *env, jclass clazz)
{
    JavaVM * javaVm = nullptr;
    env->GetJavaVM(&javaVm);

    // 打印:当前函数env地址, 当前函数jvm地址, 当前函数job地址,  JNI_OnLoad的jvm地址
    printf("nativeFun3 当前函数env地址%p,  当前函数jvm地址:%p,  当前函数clazz地址:%p, JNI_OnLoad的jvm地址:%p\n", env, javaVm, clazz, ::gVm);
    cout<< endl;

    // 调用run
    pthread_t pid;
    pthread_create(&pid, nullptr, run, nullptr);
}

JNIEXPORT void JNICALL Java_com_jni_thread_TestDemo_staticFun4
        (JNIEnv *env, jclass clazz)
{
    JavaVM * javaVm = nullptr;
    env->GetJavaVM(&javaVm);

    // 打印:当前函数env地址, 当前函数jvm地址, 当前函数job地址,  JNI_OnLoad的jvm地址
    printf("nativeFun4 当前函数env地址%p,  当前函数jvm地址:%p,  当前函数clazz地址:%p, JNI_OnLoad的jvm地址:%p\n", env, javaVm, clazz, ::gVm);
    cout<< endl;
}
public static void main(String[] args) {

    TestDemo testDemo = new TestDemo();


    System.out.println("-----------------------");
    testDemo.nativeFun1(); // main线程调用的
    testDemo.nativeFun2(); // main线程调用的
    staticFun3(); // main线程调用的

    // 第四个  new Thread 调用  ThreadClass == clasz 当前函数clazz地址
    new Thread() {
        @Override
        public void run() {
            super.run();
            staticFun4(); // Java的子线程调用
        }
    }.start();

}

RUN>

*******************?输出结果?*************************
JNI_OnLoad
-----------------------
nativeFun1 当前函数env地址0x7ff08000f1f8,  当前函数jvm地址:0x101efdfb0,  当前函数job地址:0x70000f08c9d8, JNI_OnLoad的jvm地址:0x101efdfb0

nativeFun2 当前函数env地址0x7ff08000f1f8,  当前函数jvm地址:0x101efdfb0,  当前函数job地址:0x70000f08c9d8, JNI_OnLoad的jvm地址:0x101efdfb0

nativeFun3 当前函数env地址0x7ff08000f1f8,  当前函数jvm地址:0x101efdfb0,  当前函数clazz地址:0x70000f08c9d0, JNI_OnLoad的jvm地址:0x101efdfb0

run jvm地址:0x101efdfb0,  当前run函数的newEnv地址:0x7ff0800939f8 

nativeFun4 当前函数env地址0x7ff07f88d1f8,  当前函数jvm地址:0x101efdfb0,  当前函数clazz地址:0x700010142aa8, JNI_OnLoad的jvm地址:0x101efdfb0

JNI开发-线程操作插图
image-20210507143740575

JNIEnv类型是一个指向全部JNI方法的指针,JNIEnv 提供了大部分 JNI 函数。JNIEnv只在创建它的线程有效,<font color='red'>不能跨线程传递</font>,不能再线程之间共享 JNIEnv

JNI开发-线程操作插图1
image-20210507144234891
JNI开发-线程操作插图2
image-20210507144440914
JNI开发-线程操作插图3
image-20210507144544496
代码传送门

Demo-GitHub

赞(0) 打赏
未经允许不得转载:IDEA激活码 » JNI开发-线程操作

一个分享Java & Python知识的社区