内容发布更新时间 : 2024/12/23 1:58:21星期一 下面是文章的全部内容请认真阅读。
上面这段attach.c代码是invoke.c的一个变形。与在主线程中调用Prog.main不同,本地代码开启了五个线程。开启线程完成以后,它就会等待1秒钟让线程可以运行完毕,然后调用DestroyJavaVM来销毁JVM。而每一个线程都会把自己附加到JVM上面,然后调用Prog.main方法,最后断开与JVM的连接。 JNI_AttachCurrentThread的第三个参数需要传入NULL。JDK1.2引入了JNI_ThreadAttachArgs这个structure。它允许你向你要附加的线程传递特定的信息,如线程组等。JNI_ThreadAttachArgs这个structure的详细描述在13.2节里面,作为JNI_AttachCurrentThread的规范的一部分被提到。
当程序运行函数DetachCurrentThread时,它释放属于当前线程的所有局部引用。
运行程序,输出如下:
Hello World from thread 1 Hello World from thread 0 Hello World from thread 4 Hello World from thread 2 Hello World from thread 3
上面这些输出根据不同的线程调试策略,可能会出现不同的顺序。
第八章 多彩的JNI招数
我们已经讨论了JNI在写本地代码和向本地应用程序中集成JVM时的特征。本章接下来的部分分介绍其它的JNI特征。
8.1 JNI和线程
JVM可以做到在相同的地址空间内执行多个线程。由于多个线程可能会在同时共享资源,所以,增加了程序的复杂性。
要完全理解本章的东西,你需要对多线程编程比较熟悉,知道怎么样在JAVA中用多线程访问共享资源。
8.1.1 约束限制
如果你的本地代码要运行在多个线程中,有一些约束条件需要注意,这样的话,才能使得你的本地代码无论被多少个线程同时运行,都不会出现问题。 1、 JNIEnv指针只在它所在的线程中有效,不能跨线程传递和使用。不同线程调用一个本地方法时,传入的JNIEnv指针是不同的。 2、 局部引用只在创建它们的线程中有效,同样不能跨线程传递。但可以把局部引用转化成全局引用来供多线程使用。
8.1.2 监视器的入口和出口
监视器是JAVA平台的基本同步机制。每一个对象都可以和一个监视器绑定: synchronized (obj) {
... // synchronized block
}
本地代码中可以通过调用JNI函数来达到与上述JAVA代码中等效的同步目的。这要用到两个JNI函数:MonitorEnter负责进入同步块,MonitorExit用来函数同步块。
if ((*env)->MonitorEnter(env, obj) != JNI_OK) { ... /* error handling */ }
... /* synchronized block */
if ((*env)->MonitorExit(env, obj) != JNI_OK) { ... /* error handling */ };
运行上面这段代码时,线程必须先进入obj的监视器,再执行同步块中的代码。MonitorEnter需要传入jobject作为参数。同时,如果另一个线程已经进入了这个与jobject监视器的话,当前线程会阻塞。如果当前线程在不拥有监视器的情况下调用MonitorExit的话,会产生一个错误,并抛出一个
IllegalMonitorStateException异常。上面的代码中包含了MonitorEnter和MonitorExit这对函数的调用,在这对函数的使用时,我们一定要注意错误检查,因为这对函数有可能执行失败(比如,建立监视器的资源分配不成功等原因)。这对函数可以工作在jclass、jstring、jarray等类型上面,这些类型的共同特征是,都是jobject引用的特殊类型
有一个MonitorEnter方法,一定也要有一个与之对应的MonitorExit方法。尤其是在有错误或者异常需要处理的地方,要尤其小心。 if ((*env)->MonitorEnter(env, obj) != JNI_OK) ...; ...
if ((*env)->ExceptionOccurred(env)) { ... /* exception handling */
/* remember to call MonitorExit here */
if ((*env)->MonitorExit(env, obj) != JNI_OK) ...; }
... /* Normal execution path.
if ((*env)->MonitorExit(env, obj) != JNI_OK) ...;
调用MonitorEnter而不调用MonitorExit的话,很可能会引起死锁。通过上面这段代码和本节开始时的JAVA代码的比较,你一定能发现用JAVA来进行同步要方便的多,所以,尽量用JAVA来做同步吧,把与同步相关的代码都挪到JAVA中去吧。
8.1.3 监视器等待和唤醒
JAVA还提供了其它一些和线程监视器有关的API:Object.wait、Object.notify、Object.notifyAll。因为监视器等待和唤醒操作没有进入和退出操作对时效性要求那么高,所以,没有提供与这些方法相对应的JNI函数。我们可以通过JNI调用JAVA的机制来调用这些方法。 /* 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); }
上例中,我们假设Object.wait、Object.notify和Object.notifyAll已经在其它地方计算好并缓存在全局引用里面了。
8.1.4 在任意地方获取JNIEnv指针
前面我们提到了,JNIEnv指针只在当前线程中有效。那么有没有办法可以从本地代码的任意地方获取到JNIEnv指针呢?比如,一个操作系统的回调函数中,本地代码是无法通过传参的方式获取到JNIEnv指针的。 可以通过调用接口(invocation interface)中的AttachCurrentThread方法来获取到当前线程中的JNIEnv指针: JavaVM *jvm; /* already set */ f() {
JNIEnv *env;
(*jvm)->AttachCurrentThread(jvm, (void **)&env, NULL); ... /* use env */ }
一旦当前线程被附加到JVM上,AttachCurrentThread函数就会返回一个属于当前线程的JNIEnv指针。
有许多方式可以获取JavaVM指针。可以在VM创建的时候记录下来,也可以通过JNI_GetCreatedJavaVMs查询被创建的虚拟机,还可以通过调用JNI函数
GetJavaVM或者定义JNI_OnLoad句柄接口。与JNIEnv不同的是,JavaVM只要被缓存在全局引用中,是可以被跨线程使用的。
JDK1.2以后提供了一个新调用接口(invocation interface)函数GetEnv,这样,你就可以检查当前线程是否被附加到JVM上,然后返回属于当前线程的JNIEnv指针。如果当前线程已经被附加到VM上的话,GetEnv和AttachCurrentThread在功能上是等价的。
第九章 使用己有的本地库
JNI的一个使用方式就是编写一些本地方法来使用己有的本地库。本章介绍了一种生成一个包含一系列本地函数的类库的经典的方式。
本章首先用一对一映射这种(one-to-one mapping)最直接的方式来写封装类.接下来,我们会介绍一种叫做共享stubs(shared stubs)的技术来简化编写封装类的任务。然后,在本章的最后,我们会讨论怎么样使用peer classes来封装本地数据结构。
本章介绍的方式都是通过本地方法直接使用一个本地库,这样的话,应用程序调用本地方法时会依赖于本地库。这样应用程序只能运行在支持这个本地库的操作系统上面。一个更好的办法是声明一些与操作系统无关的本地方法,让这些方法来调用本地库。这样,当我们移植程序时,只需要修改这些实现中间层本地方法的本地函数就可以了,而不必动应用程序和这些中间层本地方法。
9.1 一对一映射(one-to-one mapping)
我们从一个简单的例子开始。假设我们想写一个封装类,它向标准C库提供atol函数:long atol(const char *str);
这个函数解析一个字符串并返回十进制数字。首先,我们像下面这样写: public class C {
public static native int atol(String str); ... }
为了演示如何使用C++进行JNI编程,我们用C++来实现本地方法: JNIEXPORT jint JNICALL
Java_C_atol(JNIEnv *env, jclass cls, jstring str) {
const char *cstr = env->GetStringUTFChars(str, 0); if (cstr == NULL) {
return 0; /* out of memory */ }
int result = atol(cstr);
env->ReleaseStringUTFChars(str, cstr); return result; }
9.2 Shred Stubs
一对一映射要求你为每一个你想封装的本地函数写一个stub函数,那么,当你需要为大量本地函数写封装类时,你的工作会很烦琐。本节中,我们介绍shared stubs的思想来简化工作量。
Shared stubs负责把调用者的请求分发到相应的本地函数,并负责把调用者提供的参数类型转化成本地函数需要的类型。 我们先看一下shared stubs怎么样简化C.atol方法的实现,然后还会介绍一个使用了shared stub思想的类CFunction。 public class C {
private static CFunction c_atol =
new CFunction(\ \ \ public static int atol(String str) {
return c_atol.callInt(new Object[] {str}); } ... }
C.atol不再是一个本地方法,而是使用CFunction类来定义。这个类内部实现了一个shared stub。静态变量C.c_atol存储了一个CFunction对象,这个对象对应了msvcrt.dll库中的C函数atol。一旦c_atol这个字段初始化,对C.atol的调用只需要调用c_atol.callInt这个shared stub。 一个CFunction类代表一个指向C函数的指针。 CFunction的类层次结构图如下:
public class CFunction extends CPointer {
public CFunction(String lib, // native library name String fname, // C function name String conv) { // calling convention ... }
public native int callInt(Object[] args); ... }
callInt方法接收一个java.lang.Object对象的数组作为参数,它检查数组中每一个元素的具体类型,并把它们转化成相应的C类型(比如,把String转化成char*)。然后把它们传递给相应的C函数,最后返回一个int型的结果。