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

261 阅读7分钟

一 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操作的是底层硬件资源,如果分配内存出现问题,就很容易造成系统奔溃;越强大的工具危险性越高;