七、【Java 并发】原子性之CAS操作,Unsafe 类

2021-08-25  本文已影响0人  deve_雨轩

Java 中的 CAS 操作

CAS ( Compare and Swap ) 是对一种处理器指令(例如 x86 处理器中的 cmpxchg 指令)的称呼。不少多线程相关的 Java 标准库类的实现最终都会借助 CAS。虽然在实际工作中多数情况下我们并不需要直接使用 CAS,但是理解CAS 有助于我们更好地理解相关标准库类,以便恰当地使用它们。

在 Java 中,锁在并发编程中占据了一席之地,但是使用锁后,势必会产生由于使用锁而导致线程上下文的切换和重新调度开销。Java 提供了非阻塞的 volatile 关键字来解决共享变量的可见性问题,这在一定程度。上弥补了锁带来的开销问题,但是 volatile 只能保证共享变量的可见性,不能解决读改写等的原子性问题。CAS 即 Compare and Swap,其是 JDK 提供的非阻塞原子性操作,它通过硬件保证了更新操作的原子性。JDK 里面的 Unsafe 类提供了一系列的compareAndSwap* 方法,比如下面的compareAndSwapLong 方法。

其中 compareAndSwap 的意思是比较并交换。CAS有四个操作数,分别为:

其操作含义是,如果对象 obj 中内存偏移量为 valueOffset 的变量值为 expect,则使用新的值 update 替换旧的值 expect。这是处理器提供的一个原子性指令。

关于 CAS 操作有个经典的 ABA 问题,具体如下:

假如线程 I 使用 CAS 修改初始值,为 A 的变量 X,那么线程 I 会首先去获取当前变量 X 的值(为A),然后使用 CAS 操作尝试修改 X 的值为 B,如果使用CAS操作成功了,那么程序运行一-定是正确的吗?

其实未必,这是因为有可能在线程 I 获取变量 X 的值 A 后,在执行 CAS 前,线程 II 使用 CAS 修改了变量 X 的值为 B,然后又使用CAS修改了变量 X 的值为 A。所以虽然线程 I 执行 CAS时 X 的值是 A,但是这个A已经不是线程 I 获取时的 A 了。这就是 ABA 问题。

ABA 问题的产生是因为变量的状态值产生了环形转换,就是变量的值可以从 A 到 B,然后再从 B 到 A。如果变量的值只能朝着一个方向转换,比如 A 到 B,B 到 C,不构成环形,就不会存在问题。JDK 中的AtomicStampedReference 类给每个变量的状态值都配备了一个时间戳,从而避免了ABA问题的产生。

Unsafe 类

JDK 的 rt,jar 包中的 Unsafe 类提供了硬件级别的原子性操作,Unsafe 类中的方法都是 native 方法,它们使用 JNI 的方式访问本地 C++ 实现库。

Unsafe 类重要方法

简单实用 Unsafe 类

public class UnsafeTest {

    //获取 Unsafe 实例
    static Unsafe unsafe = Unsafe.getUnsafe();

    //用于记录 state 属性的内存偏移量
    static long stateOffset;

    private volatile long state = 0;

    public long getState() {
        return state;
    }

    static{
        try {

            //获取 state 属性的内存偏移量
            stateOffset = unsafe.objectFieldOffset(UnsafeTest.class.getDeclaredField("state"));

        }catch (Exception ex){}
    }

    public static void main(String[] args) {


        UnsafeTest ut = new UnsafeTest();

        //输出 state 值
        System.out.println(ut.getState());

        //修改 state 值
        boolean success = unsafe.compareAndSwapInt(ut, stateOffset, 0, 1);

        //输出知否修改成功
        System.out.println(success);

        //输出修改过后的 state 值
        System.out.println(ut.getState());

    }

}

运行上面的代码,会很意外的报错了:

image.png

为了找出原因,我们来看看 Unsafe.getUnsafe() 方法的源码

 @CallerSensitive
public static Unsafe getUnsafe() {
    //获取调用 getUnsafe 方法的对象的 Class 对象,这里是 TestUnsafe.claass
    Class var0 = Reflection.getCallerClass();
    // 判断是不是 Bootstrap 类加载器加载的 localClass,很显然 TestUnsafe.class 是使用 AppClassLoader 加载的,所以直接抛出了异常
    if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
        throw new SecurityException("Unsafe");
    } else {
        return theUnsafe;
    }
}

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

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

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

public class UnsafeTest {

    //Unsafe 实例
    static Unsafe unsafe;
    //用于记录 state 属性的内存偏移量
    static long stateOffset;

    private volatile long state = 0;

    public long getState() {
        return state;
    }

    static {
        try {
            //使用反射获取 Unsafe 的成员变量 theUnsafe
            Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");

            //设置为可存取
            theUnsafeField.setAccessible(true);

            // 获取该变莹的值
            unsafe = (Unsafe) theUnsafeField.get(null);

            //获取 state 属性的内存偏移量
            stateOffset = unsafe.objectFieldOffset(UnsafeTest.class.getDeclaredField("state"));

        } catch (Exception ex) {
        }
    }

    public static void main(String[] args) {

        UnsafeTest ut = new UnsafeTest();

        //输出 state 值
        System.out.println(ut.getState());

        //修改 state 值
        boolean success = unsafe.compareAndSwapInt(ut, stateOffset, 0, 1);

        //输出知否修改成功
        System.out.println(success);

        //输出修改过后的 state 值
        System.out.println(ut.getState());

    }

}
上一篇 下一篇

猜你喜欢

热点阅读