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

image-20210507143740575

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

image-20210507144234891
image-20210507144440914
image-20210507144544496
代码传送门

Demo-GitHub

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,386评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,142评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,704评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,702评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,716评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,573评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,314评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,230评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,680评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,873评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,991评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,706评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,329评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,910评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,038评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,158评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,941评论 2 355

推荐阅读更多精彩内容