前言本文是笔者在日常开发过程中遇到的关于JUC(java.util.concurrent)中CAS、ABA问题以及AtomicReference相关类的设计思考的一些记录.对于需要处理ABA问题的读者,或者像作者一样有设计问题探索好奇心的读者,或许会有一定的启发。本文主体由三部分组成:首先讲解多线程场景数据同步的常用语言工具,然后是什么是ABA问题,产生的原因和可能造成的影响,再探究JUC官方的工具设计解决ABA问题文末会对多线程数据同步的常用解决方案做一个简短的经验总结和概括。受限于作者的理解和知识水平,文章中的一些术语难免有失偏颇。对于有歧义或者有争议的地方,欢迎大家讨论指正。一、异步场景常用工具Java中多线程数据同步的场景经常出现:关键字volatile关键字synchronized可重入锁/读写锁java.util.concurrent.locks.*容器同步封装,比如Collections。synchronizedXxx()新的线程安全容器,如CopyOnWriteArrayList/ConcurrentHashMap阻塞队列java.util.concurrent.BlockingQueue原子类java.util.concurrent.atomic.*以及JUC中的其他工具如CountDownLatch/Exchanger/FutureTask等角色。其中volatile关键字用于刷新数据缓存,即保证A线程修改某个数据后,在B线程中可以看到。这里涉及的线程缓存和指令重排由于篇幅原因不在本文讨论范围之内。无论是synchronized关键字下的对象锁,还是基于同步器AbstractQueuedSynchronizer的Lock实现,都是悲观锁。但是在同步容器封装、新的线程安全容器、阻塞队列中使用了悲观锁;但是每种类型内部使用不同的Lock实现类和JUC工具,不同的容器有不同的加锁粒度和加锁策略。分别进行处理和优化。这里值得一提的是,本文的重点是原子类,即java.util.concurrent.atomic.*包下的几个类库,如AtomicBoolean/AtomicInteger/AtomicReference2.CAS和ABA问题我们知道我们在使用悲观锁的场景下,如果一个线程抢占了锁,那么其他想要获取锁的线程就必须阻塞等待,直到被锁线程计算完成释放锁资源。现代CPU提供硬件级指令来实现同步原语,这意味着线程可以在运行期间检测其他线程是否也在读写同一内存。基于此,Java提供了一个忙循环而不是阻塞的一系列工具类AutomicXxx,它是乐观锁的一种实现。它的常规使用如下:compareAndSet方法由Native层的硬件指令实现if(!isRequesting.compareAndSet(false,true)){return;}try{//dosth...}finally{isRequesting.set(false)}}}复制代码进入JDK11AtomicBoolean的源码,可以看到compareAndSet最终调用Native层的方式如下。事实上,在旧版本中,JDK是使用Unsafe类进行处理的。在输入参数中,有传入状态变量的字段偏移值。新版本将两者封装成VarHandle,并使用DL查找依赖(笔者猜测可能与JDK9相关的模块化改造相同)://老版本publicclassAtomicBoolean{privatestaticfinalsun.misc.UnsafeU=sun.misc.Unsafe.getUnsafe();privatestaticfinallongVALUE;static{try{VALUE=U.objectFieldOffset(AtomicBoolean.class.getDeclaredField("value"));}catch(ReflectiveOperationExceptione){thrownewError(e);}}privatevolatileint值;publicfinalbooleancompareAndSet(booleanexpect,booleanupdate){returnU.compareAndSwapInt(this,VALUE,(expect?1:0),(update?1:0));}}//新版本publicclassAtomicBoolean{privatestatic最终的VarHandle值;静态{尝试{MethodHandles.Lookupl=MethodHandles.lookup();VALUE=l.findVarHandle(AtomicBoolean.class,"value",int.class);}catch(ReflectiveOperationExceptione){thrownewExceptionInInitializerError(e);}}私有易失性整数value;publicfinalbooleancompareAndSet(booleanexpectedValue,booleannewValue){returnVALUE.compareAndSet(this,(expectedValue?1:0),(newValue?1:0));}}复制代码就像进入仓库一样和value偏移值,Native层可以根据这两个值定位到一定的栈内存,所以对于基本类型是没有问题的。在原子类型系统中,AtomicReference是用来引用复合类型实例的,而Java中的Object类型只是把堆保存在栈上。对象中对象数据块的地址,其结构如下:在实际运行过程中,在调用AtomicReference#compareAndSet()时,Native层只会比较栈中内存的值,不会进行支付注意它指向的堆中的数据。这可能有点抽象,看一段实验代码:StringBuildervarA=newStringBuilder("abc");StringBuildervarB=newStringBuilder("123");AtomicReference
