JUC【4】Unsafe和Atomic

275 阅读4分钟

参考

juejin.cn/post/697799…

tech.meituan.com/2019/02/14/…

Unsafe - 线程类基石

在AQS中我们大量接触到了CAS操作,比如:

 /**
  * CAS head field. Used only by enq.
  */
 private final boolean compareAndSetHead(Node update) {
     return unsafe.compareAndSwapObject(this, headOffset, null, update);
 }
 ​

这里我们注意到:这里的CAS,是使用Unsafe来实现的。

这里简单复习一下CAS:

CAS是compare and swap 的缩写

简单点来说:

  • 因为JVM的设计初衷是尽可能地减少编程过程中内存的申请释放问题,因此java中并没有直接提供对应的功能,而是在JVM中做了这一工作;
  • 但在一些性能要求高,或者需要调用对应内存地址的情况下,这一设计就很碍手碍脚。

因此,Java中提供了一个Unsafe类,来做这些操作,可以这么说:Unsafe给java程序员提供了一个类似C、Cpp语言中的部分能力。

然而,在JDK之外,Unsafe并不能通过Unsafe.getUnsafe()来获取,这部分在源码里可以看到:

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

也就是说,

这个方法只有在引导类加载器加载Unsafe类是调用才合法,否则会抛出一个SecurityException异常

可以通过反射的方式,来获取Unsafe:

         try {
             Field field = Unsafe.class.getDeclaredField("theUnsafe");
             field.setAccessible(true);
             Unsafe unsafe = (Unsafe) field.get(null);
         } catch (Exception e) {
             e.printStackTrace();
         }
 作者:赌一包辣条
 链接:https://juejin.cn/post/6977993272538955806
 来源:稀土掘金
 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Unsafe还提供了线程相关的其他一些底层支持,比如:

  • unsafe提供了volatile关键字中所隐式引入的内存屏障的概念的使用:

     //内存屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前
     public native void loadFence();
     //内存屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前
     public native void storeFence();
     //内存屏障,禁止load、store操作重排序
     public native void fullFence();
    

    有这个工具,就可以使用Java代码而不是通过JVM底层的实现来进行屏障操作,这个的典型示例就是JDK1.8引入的StampedLock。

  • unsafe提供了线程挂起的功能使用:

     public native void unpark(Object var1);
     ​
     public native void park(boolean var1, long var2);
    

    还记得AQS里,获取不到锁就挂起直到持有锁线程退出时会唤醒后续线程吗?就是下面这俩:

     private final boolean parkAndCheckInterrupt() {
        //这里就挂起了
         LockSupport.park(this);
         return Thread.interrupted();
     }
     ​
         public final boolean release(int arg) {
             if (tryRelease(arg)) {
                 Node h = head;
                 if (h != null && h.waitStatus != 0)
                     //这里就会唤醒了
                     unparkSuccessor(h);
                 return true;
             }
             return false;
         }
    

    而这里的park和unpark,最后落地到线程上就是通过这个park和unpark了。

Atomic - CAS封装

上面也提到了:

  • Unsafe是不支持直接获取的

而很多情况下其实我们并不需要Unsafe这么重的东西,只是要使用CAS的功能,因此我们希望,能够提供一个封装好的CAS给我们用。

jdk里的Atomic就是基于这个前提下封装出来的。

我们就拿AtomicInteger为案例,来看看其中的具体结构:

     public static void main(String[] args) {
         AtomicInteger ai = new AtomicInteger(1);
         ai.getAndIncrement();
         ai.compareAndSet(ai.get(),100);
     }

对于CAS最经典的方法就是compareAndSet了,也有地方是CompareAndSwap的。

这里的compareAndSet是会返回false的,也就是说:要保证更新成功,我们必须这么做:

 while(!ai.compareAndSet(ai.get(),10))

毫无疑问在竞争强的case里这里会导致CPU的大量自旋操作浪费CPU资源。

一般来说AtomicInteger使用最多的是getAndIncrement。

在Atomic类里,会封装一个起始地址:

static {
    try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}

这里的valueOffset,指的并不是实际的地址,而是这个valueOffset相对于类起始位置的偏移地址。

在CAS操作中,我们知道是要有目标内存地址的,我们CAS操作比较以及写入的是跟这个地址相关的。

public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

通过这个CAS的使用,能够反推Unsafe中,CAS的入参含义:

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
  • Object:我们期望发生CAS操作的对象(假设地址为A)
  • var2:我们期望发生CAS操作的字段和对象起始位置的偏移地址(假设地址为B)
  • var4:期望原有值
  • var5:期望写入值

映射到操作上,那就是:

  • 我们期望将var5写入地址为A+B位置的变量;写入前比较A+B上的值,如果不为var4,则返回false,否则则写入。

\