• 早点学会Unsafe和CAS早下班陪女朋友


    一 Unsafe类常用API了解

    今天的内容是Unsafe类,学习原子类的底层实现,并发编程中的基石之一,也是JDK源码中的重要成员。

    Java无法直接访问底层操作系统,而是通过本地(native)方法来访问。不过尽管如此,JDK中有一个Unsafe类提供了硬件级别原子操作,它们使用JIN的方式实现C++;由于是硬件级别的操作API,我们平时几乎无法遇见,因为它是提供给JDK内部使用,我们也使用不到,不过我们在看JDK源码的时候还是能经常见到它们的身影;

    先了解一些unsafe一些常用的API

    先看第一组获取偏移值

    • 返回变量在类中的内存偏移值;
    public native long objectFieldOffset(Field var1);
    
    • 获取数组中第一个元素所在的偏移地址
    public native int arrayBaseOffset(Class<?> var1);
    
    • 获取数组中第一个元素所占用的字节
    public native int arrayIndexScale(Class<?> var1);
    

    其次看第二组内存分配

    // 分配内存
    public native long allocateMemory(long var1);
    // 扩展内存
    public native long reallocateMemory(long var1, long var3);
    // 指定对象设置指定内存值
    public native void setMemory(Object var1, long var2, long var4, byte var6);
    // 释放内存
    public native void freeMemory(long var1);
    

    再看看第三组 CAS(compareAndSwap

    解释语义:当obj对象中的偏移为offse的变量值与期望值expect值相等时,就使用update更新obj;成功返回true,失败返回false

    //  对象CAS
    public final native boolean compareAndSwapObject(Object obj, long offset, Object expect, Object update);
    // int CAS
    public final native boolean compareAndSwapInt(Object obj, long offset, int expect, int update);
    // long CAS
    public final native boolean compareAndSwapLong(Object obj, long offset, long expect, long update);
    

    看 一组 Volatile 语义;

    这边只列出object对象使用方式,其实还有其它8大基本数据类型,使用方式一样;

    // 获取 obj 对象 中偏移为 offset 的 Volatile语义值
    public native Object getObjectVolatile(Object obj, long offset);
    // 设置 obj 对象 中偏移为 offset 的 Volatile语义值
    public native void putObjectVolatile(Object obj, long offset, Object value);
    

    CAS和 Volatile 语义衍生的一组

    对象obj 的偏移值 为offset的Volatile 语义值 则用 update 更新 Volatile 语义值 var5;注意返回的是旧值var5;

        public final Object getAndSetObject(Object obj, long offset, Object update) {
            Object var5;
            do {
                var5 = this.getObjectVolatile(obj, offset);
            } while(!this.compareAndSwapObject(obj, offset, var5, update));
    
            return var5;
        }
    

    对象 obj的偏移 为 offset 的 变量Volatile 语义 为 var6, 则用 var6 + add 的值 更新 var6;

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

    Park/Unpark 组合主要是JVM用来切换线程;Park 为阻塞当前线程,Unpark 为唤醒线程;

    最后看一组 putOrdered 操作;设置 对象obj 偏移为 offset 的变量值为 value, 支持 violate语义

        public native void putOrderedObject(Object obj, long ofsset, Object value);
    
        public native void putOrderedInt(Object obj, long ofsset, int value);
    
        public native void putOrderedLong(Object obj, long ofsset, long value);
    

    二 原子类使用分析

    我们都知道原子类是线程安全的原子性操作;我们先来熟悉下如何操作原子类,验证是否真的是线程安全;

    r如下代码中 使用 getAndIncrement 方法对 atomicInteger 变量进行自增;启动2 个线程后运行atomicInteger结果为正确值;

    public class UnsafeTest4 {
    
        private static AtomicInteger atomicInteger = new AtomicInteger();
        private static volatile Integer count = 0;
    
        public static void main(String[] args) throws InterruptedException {
            Runnable runnable = () -> {
                for (int i = 0; i < 10000; i++) {
                    atomicInteger.getAndIncrement();
                    count++;
                }
            };
            // 启动2 个线程
            Thread thread1 = new Thread(runnable);
            Thread thread2 = new Thread(runnable);
            thread1.start();
            thread2.start();
            // 携程
            thread1.join();
            thread2.join();
            // atomicInteger=20000
            System.out.println("atomicInteger=" + atomicInteger);
            // count=13401
            System.out.println("count=" + count);
        }
    }
    

    getAndIncrement 方法是如何做到 原子性操作呢?我们 试着从源码角度分析, 内部其实就是 使用 unsafe 类 getAndAddInt 方法, 与之前分析 getAndAddLong 的 效果功能差不多;

       public final int getAndIncrement() {
            return unsafe.getAndAddInt(this, valueOffset, 1);
        }
    

    瞧一眼getAndAddInt即可, obj 的偏移为offset 的变量 Volatile语义值为 var5 , 使用var5 + addValue 更新 var5;

        public final int getAndAddInt(Object obj, long offset, int addValue) {
            int var5;
            do {
                var5 = this.getIntVolatile(obj, offset);
            } while(!this.compareAndSwapInt(obj, offset, var5, var5 + addValue));
    
            return var5;
        }
    

    原子类的线程安全操作其实底层就是使用CAS操作;

    三 CAS使用与验证

    我们无法直接使用 Unsafe 类,如果按照jdk源码中给出的示例调用我们会撞的头破血流

        public static void main(String[] args)  {
            Unsafe unsafe = Unsafe.getUnsafe();
    
        }
    

    错误信息如下

    Exception in thread "main" java.lang.SecurityException: Unsafe
    	at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
    	at com.youku1327.base.cas.UnsafeTest.main(UnsafeTest.java:15)
    
    Process finished with exit code 1
    

    源码判定只要调用者的类加载器不是系统域的直接报错,所以我们根本不能使用静态方式调用;

        @CallerSensitive
        public static Unsafe getUnsafe() {
            Class var0 = Reflection.getCallerClass();
            if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
                throw new SecurityException("Unsafe");
            } else {
                return theUnsafe;
            }
        }
    

    java提供了强大的反射机制能够让我们调用Unsafe类;

        private static Unsafe getUnsafe(){
            // 通过反射获取 unsafe
            Unsafe unsafe = null;
            try {
                Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
                Field theUnsafe = unsafeClass.getDeclaredField("theUnsafe");
                theUnsafe.setAccessible(true);
                unsafe = (Unsafe)theUnsafe.get(null);
            } catch (Exception e) {
                e.printStackTrace();
            }
           return  unsafe;
        }
    

    知识追寻者使用 unsafe 的 objectFieldOffset 先计算出 变量的地址偏移,然后通过 CAS 验证该对象的偏移 是否 与计算的偏移相同;结果明显相等,掌握这一步,我们就知道如何使用CAS;

        public static void main(String[] args)  {
            try {
                UnsafeTest unsafeTest = new UnsafeTest();
                Unsafe unsafe = UnsafeTest.getUnsafe();
                long value = unsafe.objectFieldOffset(unsafeTest.getClass().getDeclaredField("name"));
                // 偏移值为12
                System.out.println(value);
                // CAS 操作 判定 unsafeTest 对象的偏移值 为 12 值是否为 kxg ; 如果是 就用 zszxz 代替
                boolean compareAndSwapObject = unsafe.compareAndSwapObject(unsafeTest ,12, "kxg", "zszxz");
                // true
                System.out.println(compareAndSwapObject);
                // zszxz
                System.out.println(unsafeTest.name);
            } catch (Exception e) {
                e.printStackTrace();
            }
    
        }
    

    早期 的Unsafe 类 还能使用 monitorEnter 和 monitorExit 模拟 synchronized 锁,多线程的安全性;由于 这两个API 在JDK1.8已经过时,不做过多讲解;

    四 CAS 存在 问题

    4.1 CAS 问题

    CAS 和 锁都能解决 多线程情况下 的原子性问题;与锁相比,它没有 锁的竞争 的 额外开销,但缺点也很明显,要不断的自旋,循环时间非常长;只能保证一个变量的原子性操作;存在ABA问题

    关注公众号:知识追寻者领取面试题集

    4.2 ABA 问题

    其它都好理解,着重说下什么是ABA问题

    如果一个值原来是A,变成了B,然后又变成了A,那么在CAS检查的时候会发现没有改变,但是实质上它已经发生了改变,这就是所谓的ABA问题;

    变量A 变为B,B再变为 A的过程中;线程 N 拿到的A在CAS之前是 初始变量A吗? 显然不一定 ,线程M 将 A 经过CAS 变为B,线程 M再 将变量 B再经过 CAS 变为 A; 线程N获取后面的A 与前面的A 就不是同一个变量;

    ABA问题的解决

    CAS解决ABA 问题的关键就是 使用版本号; A1---> B2 ---> A3 , 就明显区分了不同的变量;

    原子类之AtomicStampedReference可以解决ABA问题,它内部不仅维护了对象值,还维护了一个Stamp(可以理解为版本号) ,使用 compareAndSet 方法就可以实现无锁自旋;

    我们可以看下 AtomicStampedReference 类源码;

    	// 参数为:期望值 新值 期望版本号 新版本号
        public boolean compareAndSet(V expectedReference, V
                newReference, int expectedStamp, int newStamp);
    
        //获得当前对象引用
        public V getReference();
    
        //获得当前版本号
        public int getStamp();
    
        //设置当前对象引用和版本号
        public void set(V newReference, int newStamp);
        
        //如果当前引用等于预期引用, 将更新新的版本号到内存
    	public boolean attemptStamp(V expectedReference, int newStamp)
    	
    	//构造方法, 传入引用和版本号
    	public AtomicStampedReference(V initialRef, int initialStamp)
    
    

    4.3 验证 AtomicStampedReference 解决 ABA 问题

    使用 AtomicStampedReference 来模拟 CAS 的 ABA 问题,我们对其加版本号后,CAS后的结果肯定为失败;

    public class UnsafeTest3 {
    
        // 初始值10,版本号0
        private static AtomicStampedReference<Integer> count = new AtomicStampedReference<>(10, 0);
        private static final Logger logger = LoggerFactory.getLogger(UnsafeTest3.class);
    
        public static void main(String[] args) {
            new Thread(() -> {
                //获取当前版本
                int stamp = count.getStamp();
                logger.info("线程{} 当前版本{}",Thread.currentThread(),stamp);
                try {
                    //等待1秒
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //此时expectedReference未发生改变,但是stamp已经被修改了,所以CAS失败
                boolean isCASSuccess = count.compareAndSet(10, 12, stamp, stamp + 1);
                logger.info("CAS是否成功? {}",isCASSuccess);
            }, "主操作线程").start();
    
            new Thread(() -> {
                //获取当前版本
                int stamp = count.getStamp();
                logger.info("线程{} 当前版本{}",Thread.currentThread(),stamp);
                count.compareAndSet(10, 12, stamp, stamp + 1);
                logger.info("线程{} 增加后版本{}",Thread.currentThread(),count.getStamp());
    
                // 模拟ABA问题 先更新成12 又更新回10
    
                //获取当前版本
                int newStamp = count.getStamp();
                count.compareAndSet(12, 10, newStamp, newStamp + 1);
                logger.info("线程{} 减少后版本{}",Thread.currentThread(),count.getStamp());
            }, "干扰线程").start();
        }
    
    
    }
    

    输出结果:

    线程Thread[主操作线程,5,main] 当前版本0
    线程Thread[干扰线程,5,main] 当前版本0
    线程Thread[干扰线程,5,main] 增加后版本1
    线程Thread[干扰线程,5,main] 减少后版本2
    CAS是否成功? false
    

    Unsafe类功能这么强大,为什么JDK不开给我用,而是限制JDK内部使用呢?个人觉得就是因为Unsafe操作的是底层硬件资源,如果分配内存出现问题,就很容易造成系统奔溃;越强大的工具危险性越高;

  • 相关阅读:
    CodeCraft-19 and Codeforces Round #537 (Div. 2) C. Creative Snap
    51nod 1007正整数分组(01背包变形)
    51nod 1007正整数分组(01背包变形)
    Codeforces Round #533 (Div. 2) C. Ayoub and Lost Array
    Codeforces Round #533 (Div. 2) C. Ayoub and Lost Array
    小a与星际探索
    小a与星际探索
    poj3764
    心理博弈
    4级
  • 原文地址:https://www.cnblogs.com/zszxz/p/13745425.html
Copyright © 2020-2023  润新知