美文网首页
七、【Java 并发】原子性之CAS操作,Unsafe 类

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

作者: deve_雨轩 | 来源:发表于2021-08-25 17:17 被阅读0次

    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 方法。

    • boolean compareAndSwapLong(Object obj,long valueOffset,long expect, long update);

    其中 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 类重要方法

    • long objectFieldOffset(Field field);
      返回指定的变量在所属类中的内存偏移地址,该偏移地址仅仅在该 Unsafe 函数中访问指定字段时使用。

    • 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 后阻塞的线程。

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

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

    简单实用 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());
    
        }
    
    }
    

    相关文章

      网友评论

          本文标题:七、【Java 并发】原子性之CAS操作,Unsafe 类

          本文链接:https://www.haomeiwen.com/subject/fkkeiltx.html