本文是《The Java Native Interface Programmer’s Guide and Specification》读书笔记
JNI里的多线程
在本地方法里写有关多线程的代码时,需要知道下面几个约束:
- 一个JNIEnv指针只在与它关联的线程里有效,也就是说,在线程间传递JNIEnv指针和在多线程环境里通过缓存来使用它是不允许和不安全的。JVM在同一个线程里多次调用同一个本地方法时传递的是同一个JNIEnv指针,但在不同的线程里调用同一个本地方法时传递的是不同的JNIEnv指针。
- 本地引用只在创建它的线程里有效,也就是说你不能在线程间传递本地引用。因为在多线程的环境里可能会使用到相同的引用,因此我们需要将本地引用转型为全局引用。
JNI里的同步机制(类似于锁的获取与释放)
在本地方法里,可以通过JNI函数来实现Java里的同步块(互斥资源的使用),用方法MonitorEnter
来得到一个JNI引用的监控器(锁),方法MonitorExit
释放监控器(锁的释放),下面是简单的使用场景:
//省略了其他代码,下面只是本地方法实现代码里的某一部分
if ((*env)->MonitorEnter(env, obj) != JNI_OK) {
... /* 获取obj引用的锁失败,进行相应的处理 */
}
/* 编写需要同步的代码块 */
....
if ((*env)->ExceptionOccurred(env)) {
... /* 异常处理的代码 */
/* 在这里要记得调用 MonitorExit来释放所获得的监控器 */
if ((*env)->MonitorExit(env, obj) != JNI_OK) ...;
}
/*正常的调用MonitorExit来释放锁*/
if ((*env)->MonitorExit(env, obj) != JNI_OK) {
... /* 释放obj引用的锁失败,进行相应的处理 */
};
需要注意的是,在同步代码块里可能会发生异常,我们需要的对应的异常处理代码中调用MonitorExit
方法来释放锁,如果忘记调用这个释放监控器的方法,可能会导致死锁的发生。因为在JNI里使用同步机制会比较麻烦,因此我们尽可能在ava的程序里来实现相应的同步机制。
Java API中提供了一些对线程间同步非常有用的方法,如Object.wait
,Object.notify
,Object.notifyAll
来等待获取一个对象的锁,唤醒等待获得对象锁的对象等。但在JNI里并没有提供对应的方法来等待获取对象的监控器,唤醒等待获取对象监控器的对象,因此在JNI里通常采用JNI的方法调用机制来调用对应的Java方法来实现相应的操作。在下面的代码里,我们假设已经获得了相应方法的methodID缓存在全局引用中;
/* precomputed method IDs */
static jmethodID MID_Object_wait;
static jmethodID MID_Object_notify;
static jmethodID MID_Object_notifyAll;
void
JNU_MonitorWait(JNIEnv *env, jobject object, jlong timeout)
{
(*env)->CallVoidMethod(env, object, MID_Object_wait,
timeout);
}
void
JNU_MonitorNotify(JNIEnv *env, jobject object)
{
(*env)->CallVoidMethod(env, object, MID_Object_notify);
}
void
JNU_MonitorNotifyAll(JNIEnv *env, jobject object)
{
(*env)->CallVoidMethod(env, object, MID_Object_notifyAll);
}
在任意上下文中获取JNIEnv指针
在首页的介绍中,我们知道JNIEnv指针只在与它相关联的线程里是有效的,一般对于本地方法来说,他们是从JVM中得到这个指针作为方法的第一个参数的。但也有一小部分本地代码不是从JVM中直接得到这个JNIEnv指针的,比如本地方法里的某一部分代码是属于操作系统调用的某一个方法的,因此这可能就导致将JNIEnv指针作为参数是没有用的。这时我们就可以调用方法AttachCurrentThread
来得到当前线程的JNIEnv指针。只要当前这个线程已经加载到JVM中,就可以返回正确的指针。
JavaVM *jvm; /* already set */
f()
{
JNIEnv *env;
(*jvm)->AttachCurrentThread(jvm, (void **)&env, NULL);
... /* use env */
}
上面的代码里,需要先获取一个JVM的指针,JNI里有许多方法可以得到一个JVM指针,JNI_GetCreatedJavaVMs
,GetJavaVM
等。并且JVM指针是可以保存在全局引用里的。
本地代码的注册
本地代码的注册方式有两种:
- 在执行本地方法前,在Java代码里使用语句
System.loadLibrary("foo")
加载本地方法所有的链接库; - 在本地方法的实现里,需要用到其他链接库里的本地方法时,第一种方法就不适用了,比如,在本地方法里声明另一个本地方法
void JNICALL g_impl(JNIEnv *env, jobject self);
但它的实现是在另一个链接库里实现的,则需要使用下面的代码来作为这个方法的实现:
//这里某一个本地方法里的代码,省略了其他部分代码
JNINativeMethod nm;
nm.name = "g";//需要使用的其他链接库里的本地方法的名字
/* 方法的描述,如返回值,参数等 */
nm.signature = "()V";
nm.fnPtr = g_impl;//在这里的本地方法的声明的名字
//注册g_impl方法
(*env)->RegisterNatives(env, cls, &nm, 1);
方法g_imlp
的声明并不需要遵循JNI的命名规范,因为这只是调用时的方法指针,并不需要展开代码(调用这个方法时,实际调用的是另一个链接库里名为g
的JNI方法),所有不需要使用JNIEXPORT
,但需要遵循JNI的调用规范。
使用动态注册链接库的方法的好处为:
其他
将本地代码创建的string对象转型为jstring对象返回给Java程序:
jstring JNU_NewStringNative(JNIEnv *env, const char *str)
{
jstring result;
jbyteArray bytes = 0;
int len;
if ((*env)->EnsureLocalCapacity(env, 2) < 0) {
return NULL; /* out of memory error */
}
len = strlen(str);
bytes = (*env)->NewByteArray(env, len);
if (bytes != NULL) {
(*env)->SetByteArrayRegion(env, bytes, 0, len,
(jbyte *)str);
result = (*env)->NewObject(env, Class_java_lang_String,
MID_String_init, bytes);
(*env)->DeleteLocalRef(env, bytes);
return result;
} /* else fall through */
return NULL;
}
将jstring对象转型为本地string对象
char *JNU_GetStringNativeChars(JNIEnv *env, jstring jstr)
{
jbyteArray bytes = 0;
jthrowable exc;
char *result = 0;
if ((*env)->EnsureLocalCapacity(env, 2) < 0) {
return 0; /* out of memory error */
}
bytes = (*env)->CallObjectMethod(env, jstr,
MID_String_getBytes);
exc = (*env)->ExceptionOccurred(env);
if (!exc) {
jint len = (*env)->GetArrayLength(env, bytes);
result = (char *)malloc(len + 1);
if (result == 0) {
JNU_ThrowByName(env, "java/lang/OutOfMemoryError",
0);
(*env)->DeleteLocalRef(env, bytes);
return 0;
}
(*env)->GetByteArrayRegion(env, bytes, 0, len,
(jbyte *)result);
result[len] = 0; /* NULL-terminate */
} else {
(*env)->DeleteLocalRef(env, exc);
}
(*env)->DeleteLocalRef(env, bytes);
return result;