Unsafe 类中的重要方法

JDK 的 rt.jar 包中的 Unsafe 类提供了硬件级别的原子性操作,Unsafe 类中的方法都是 native 方法,它们使用 JNI 的方式访问本地 C++ 实现库。下面我们来了解一下 Unsafe 提供的几个主要的方法以及编程时如何使用 Unsafe 类做一些事情。

● long objectFieldOffset(Field field)方法:返回指定的变量在所属类中的内存偏移地址,该偏移地址仅仅在该 Unsafe 函数中访问指定字段时使用。如下代码使用 Unsafe 类获取变量 value 在 AtomicLong 对象中的内存偏移。

  1. static {
  2. try {
  3. valueOffset = unsafe.objectFieldOffset
  4. (AtomicLong.class.getDeclaredField("value"));
  5. } catch (Exception ex) { throw new Error(ex); }
  6. }

● int arrayBaseOffset(Class arrayClass)方法:获取数组中第一个元素的地址。

● int arrayIndexScale(Class arrayClass)方法:获取数组中一个元素占用的字节。

● boolean compareAndSwapLong(Object obj, long offset, long expect, long update)方法:比较对象 obj 中偏移量为 offset 的变量的值是否与 expect 相等,相等则使用 update 值更新,然后返回 true,否则返回 false。

● public native long getLongvolatile(Object obj, long offset)方法:获取对象 obj 中偏移量为 offset 的变量对应 volatile 语义的值。

● void putLongvolatile(Object obj, long offset, long value)方法:设置 obj 对象中 offset 偏移的类型为 long 的 field 的值为 value,支持 volatile 语义。

● void putOrderedLong(Object obj, long offset, long value)方法:设置 obj 对象中 offset 偏移地址对应的 long 型 field 的值为 value。这是一个有延迟的 putLongvolatile 方法,并且不保证值修改对其他线程立刻可见。只有在变量使用 volatile 修饰并且预计会被意外修改时才使用该方法

● void park(boolean isAbsolute, long time)方法:阻塞当前线程,其中参数 isAbsolute 等于 false 且 time 等于 0 表示一直阻塞。time 大于 0 表示等待指定的 time 后阻塞线程会被唤醒,这个 time 是个相对值,是个增量值,也就是相对当前时间累加 time 后当前线程就会被唤醒。如果 isAbsolute 等于 true,并且 time 大于 0,则表示阻塞的线程到指定的时间点后会被唤醒,这里 time 是个绝对时间,是将某个时间点换算为 ms 后的值。另外,当其他线程调用了当前阻塞线程的 interrupt 方法而中断了当前线程时,当前线程也会返回,而当其他线程调用了 unPark 方法并且把当前线程作为参数时当前线程也会返回。

● void unpark(Object thread)方法:唤醒调用 park 后阻塞的线程。

下面是 JDK8 新增的函数,这里只列出 Long 类型操作。

● long getAndSetLong(Object obj, long offset, long update)方法:获取对象 obj 中偏移量为 offset 的变量 volatile 语义的当前值,并设置变量 volatile 语义的值为 update。

  1. public final long getAndSetLong(Object obj, long offset, long update)
  2. {
  3. long l;
  4. do
  5. {
  6. l = getLongvolatile(obj, offset); //(1)
  7. } while (! compareAndSwapLong(obj, offset, l, update));
  8. return l;
  9. }

由以上代码可知,首先(1)处的 getLongvolatile 获取当前变量的值,然后使用 CAS 原子操作设置新值。这里使用 while 循环是考虑到,在多个线程同时调用的情况下 CAS 失败时需要重试。

● long getAndAddLong(Object obj, long offset, long addValue)方法:获取对象 obj 中偏移量为 offset 的变量 volatile 语义的当前值,并设置变量值为原始值 +addValue。

  1. public final long getAndAddLong(Object obj, long offset, long addValue)
  2. {
  3. long l;
  4. do
  5. {
  6. l = getLongvolatile(obj, offset);
  7. } while (! compareAndSwapLong(obj, offset, l, l + addValue));
  8. return l;
  9. }

类似 getAndSetLong 的实现,只是这里进行 CAS 操作时使用了原始值 + 传递的增量参数 addValue 的值。

如何使用 Unsafe 类

看到 Unsafe 这个类如此厉害,你肯定会忍不住试一下下面的代码,期望能够使用 Unsafe 做点事情。

  1. public class TestUnSafe {
  2. //获取 Unsafe 的实例(2.2.1)
  3. static final Unsafe unsafe = Unsafe.getUnsafe();
  4. //记录变量 state 在类 TestUnSafe 中的偏移值(2.2.2)
  5. static final long stateOffset
  6. //变量(2.2.3)
  7. private volatile long state=0
  8. static {
  9. try {
  10. //获取 state 变量在类 TestUnSafe 中的偏移值(2.2.4)
  11. stateOffset = unsafe.objectFieldOffset(TestUnSafe.class.
  12. getDeclaredField(「state」));
  13. } catch Exception ex {
  14. System.out.println(ex.getLocalizedMessage());
  15. throw new Errorex);
  16. }
  17. }
  18. public static void mainString[] args {
  19. //创建实例,并且设置 state 值为 1(2.2.5)
  20. TestUnSafe test = new TestUnSafe();
  21. //(2.2.6)
  22. Boolean sucess = unsafe.compareAndSwapInttest stateOffset 0 1);
  23. System.out.printlnsucess);
  24. }
  25. }

在如上代码中,代码(2.2.1)获取了 Unsafe 的一个实例,代码(2.2.3)创建了一个变量 state 并初始化为 0。

代码(2.2.4)使用 unsafe.objectFieldOffset 获取 TestUnSafe 类里面的 state 变量,在 TestUnSafe 对象里面的内存偏移量地址并将其保存到 stateOffset 变量中。

代码(2.2.6)调用创建的 unsafe 实例的 compareAndSwapInt 方法,设置 test 对象的 state 变量的值。具体意思是,如果 test 对象中内存偏移量为 stateOffset 的 state 变量的值为 0,则更新该值为 1。

运行上面的代码,我们期望输出 true,然而执行后会输出如下结果。

Unsafe 类 - 图1

为找出原因,必然要查看 getUnsafe 的代码。

  1. private static final Unsafe theUnsafe = new Unsafe();
  2. public static Unsafe getUnsafe()
  3. {
  4. //(2.2.7)
  5. Class localClass = Reflection.getCallerClass();
  6. //(2.2.8)
  7. if (! VM.isSystemDomainLoader(localClass.getClassLoader())) {
  8. throw new SecurityException(「Unsafe」);
  9. }
  10. return theUnsafe
  11. }
  12. //判断 paramClassLoader 是不是 BootStrap 类加载器(2.2.9)
  13. public static boolean isSystemDomainLoaderClassLoader paramClassLoader
  14. {
  15. return paramClassLoader == null
  16. }

代码(2.2.7)获取调用 getUnsafe 这个方法的对象的 Class 对象,这里是 TestUnSafe. class。

代码(2.2.8)判断是不是 Bootstrap 类加载器加载的 localClass,在这里是看是不是 Bootstrap 加载器加载了 TestUnSafe.class。很明显由于 TestUnSafe.class 是使用 AppClassLoader 加载的,所以这里直接抛出了异常。

思考一下,这里为何要有这个判断?我们知道 Unsafe 类是 rt.jar 包提供的,rt.jar 包里面的类是使用 Bootstrap 类加载器加载的,而我们的启动 main 函数所在的类是使用 AppClassLoader 加载的,所以在 main 函数里面加载 Unsafe 类时,根据委托机制,会委托给 Bootstrap 去加载 Unsafe 类。

如果没有代码(2.2.8)的限制,那么我们的应用程序就可以随意使用 Unsafe 做事情了,而 Unsafe 类可以直接操作内存,这是不安全的,所以 JDK 开发组特意做了这个限制,不让开发人员在正规渠道使用 Unsafe 类,而是在 rt.jar 包里面的核心类中使用 Unsafe 功能。

如果开发人员真的想要实例化 Unsafe 类,那该如何做?

方法有多种,既然从正规渠道访问不了,那么就玩点黑科技,使用万能的反射来获取 Unsafe 实例方法。

  1. public class TestUnSafe {
  2. static final Unsafe unsafe
  3. static final long stateOffset
  4. private volatile long state = 0
  5. static {
  6. try {
  7. //使用反射获取 Unsafe 的成员变量 theUnsafe
  8. Field field = Unsafe.class.getDeclaredField(「theUnsafe」);
  9. // 设置为可存取
  10. field.setAccessibletrue);
  11. // 获取该变量的值
  12. unsafe = Unsafe field.getnull);
  13. //获取 state 在 TestUnSafe 中的偏移量
  14. stateOffset = unsafe.objectFieldOffset(TestUnSafe.class.
  15. getDeclaredField(「state」));
  16. } catch Exception ex {
  17. System.out.println(ex.getLocalizedMessage());
  18. throw new Errorex);
  19. }
  20. }
  21. public static void main(String[] args) {
  22. TestUnSafe test = new TestUnSafe();
  23. Boolean sucess = unsafe.compareAndSwapInt(test, stateOffset, 0, 1);
  24. System.out.println(sucess);
  25. }
  26. }

在如上代码中,通过代码(2.2.10)、代码(2.2.11)和代码(2.2.12)反射获取 unsafe 的实例,运行后输出结果如下。

Unsafe 类 - 图2