乐趣区

关于java:聊聊多线程中常用的Unsafe

前言

看过 JUC 源码的同学对 Unsafe 应该都不生疏,很多 JUC 包下的类外部都是用它来实现的 CAS 操作,可见它的威力和重要性。为什么它的命名是“不平安”呢?因为它能够间接拜访和操作底层内存资源,这对于程序员来说,如果适度或者不正确的应用 Unsafe 类,就变得不平安了。

如何获取它的实例?

咱们先来看看它的内部结构:

public final class Unsafe {
    private static final Unsafe theUnsafe;
    
    private Unsafe() {}
    
    @CallerSensitive
    public static Unsafe getUnsafe() {
        // 获取调用方的 Class
        Class var0 = Reflection.getCallerClass();
        // 判断调用方是否是疏导类加载器加载的
        if (!VM.isSystemDomainLoader(var0.getClassLoader())) {throw new SecurityException("Unsafe");
        } else {return theUnsafe;}
   }
}

能够看到,Unsafe 类是单例的,那咱们应用程序能够间接调用 getUnsafe()办法拿到它的实例吗?无妨来试试看:

import sun.misc.Unsafe;
public class TryGetUnsafe {public static void main(String[] args) {Unsafe theUnsafe = Unsafe.getUnsafe();
        System.out.println(theUnsafe.toString());
    }
}

运行后果如下:

认真看了 getUnsafe()源码的同学,都不难理解为什么抛出这个异样,因为它有个安全控制,若调用方的 Class 不是疏导类加载器所加载的,就会抛出异样 SecurityException(“Unsafe”),它是怎么判断是否疏导类加载器加载的呢?代码也很简略:

public static boolean isSystemDomainLoader(ClassLoader var0) {return var0 == null;}

回顾下类加载机制的双亲委派模型,最顶层就是疏导类加载器,它是用 C ++ 语言实现的,不是 Java 的类,对应的 ClassLoader 实例就为 null。
那如果咱们应用程序就想拿到 Unsafe 实例,有什么方法吗?
办法有 2 种:
①从它的限度条件登程,让咱们编写的应用程序所在的类被疏导类加载器所加载即可,具体怎么做呢?
通过 Java 命令行命令 -Xbootclasspath/a 把调用 Unsafe 相干办法的类所在 jar 包门路追加到默认的 bootstrap 门路中,即:

java -Xbootclasspath/a: ${path}   // 其中 path 为调用 Unsafe 相干办法的类所在 jar 包门路 

②弱小的反射机制,间接上代码:

import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class TryGetUnsafe {public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafeField.setAccessible(true);
        Unsafe unsafe = (Unsafe)theUnsafeField.get(null);
        System.out.println(unsafe.toString());
    }
}

运行后果如下:

阐明咱们曾经胜利获取了 Unsafe 实例。

罕用办法

public native long objectFieldOffset(Field field):获取指定的属性 Field 在所属类中的内存偏移地址。这个办法很多类都会应用到,如我另一篇文章 ThreadLocalRandom 讲到的源码:

public final native boolean compareAndSwapLong(Object obj, long offset, long expect, long update):比拟对象 obj 中偏移量为 offset 的属性值,是否与预期值 expect 相等,如果相等就替换成 update 值,而后返回 true,否则返回 false。即咱们常说的 CAS 底层实现。还有相似的办法 compareAndSwapInt,compareAndSwapObject 等。
public native long getLongVolatile(Object obj, long offset):获取对象 obj 中偏移量为 offset 的属性对应 volatile 语义的值。
public native void putLongVolatile(Object obj, long offset, long value):设置对象 obj 中偏移量为 offset 的属性值为 value,这个操作同样也是带 volatile 语义的。
public final long getAndAddLong(Object obj, long offset, long addValue):获取对象 obj 中偏移量为 offset 的属性值, 并设置新值 = 旧值 +addValue,留神返回的是旧值。这个办法没有 native 修饰符,是 java 写的,源码如下:

public final long getAndAddLong(Object obj, long offset, long addValue) {
    long l;
    do {l = this.getLongVolatile(obj, offset);
    } while(!this.compareAndSwapLong(obj, offset, l, l + addValue));
    return l;
}

public native int arrayBaseOffset(Class<?> arrayClass):获取数组的第一个元素地址。
public native int arrayIndexScale(Class<?> arrayClass):获取数组每个元素占用的字节数。
public native void park(boolean isAbsolute, long time):阻塞以后线程,其中参数 isAbsolute 示意是否为相对工夫,isAbsolute 为 false 且 time 为 0 时示意始终阻塞,time 大于 0 示意期待 time 时间段后线程被唤醒,如果 isAbsoluete 为 true,那 time 示意的是某个工夫点的工夫戳毫秒值。当其余线程调用 unpark,且参数为此线程时,此线程就会被唤醒。其余线程调用 interrupt 办法时,同样也会唤醒。
public native void unpark(Object thread):唤醒某个调用 park 办法的线程 thread。
罕用的办法就介绍这么多,其实还有很多其余办法,有趣味的同学能够去翻看源码。

退出移动版