从一个AtomicInteger原子类,我们可以引申出以下一系列知识点,可以打包一起记忆:
一、什么是ABA问题?
一句话:狸猫换太子;
有A、B两个线程,线程A执行一次CAS操作需要用时10s,但是B线程执行一次CAS操作只需要2s,主内存中的原始值为1,两个线程开始时候都获取到了原始值1,但是在由于B线程很快,它先将value值修改为了2,过一会儿又修改为了1,此时对于A线程来说,它在执行compareAndAddInt方法的时候,发现前后两个数据都是1,就很乐观地认为主存中的value值没有被其他线程修改过,顺利成功执行了compareAndAddInt方法;这种问题就是所谓的ABA问题(在一个线程执行CAS的过程中,另一个线程快速地修改了主存中的value并又快速地修改回来,从而导致第一个线程误以为没有其他线程修改过value值的问题);
ABA问题是CAS的最大问题所在,必须解决;
如何解决ABA问题,需要引入另一个概念——原子引用(AtomicReference)
二、原子引用(AtomicReference)
JUC给造了很多原子类,比如AtomicInteger、AtomicLong等,但是这些只是基本类型的原子类,实际工作中,我们经常要操作的对象是例如User、Order、Customer等对象,这些类JUC是没有办法为我们事先准备的;
那怎么办呢?
就需要使用到原子引用(AtomicReference)——它是一个泛型,可以将普通类,包装成对应的原子类;
-
简单原子引用代码演示:
package com.jiguiquan.www; import java.util.concurrent.atomic.AtomicReference; /** ** 演示原子引用 * @author jiguiquan * */ public class AtomicReferenceDemo { public static void main(String[] args) { User z3 = new User("z3",22); User li4 = new User("li4", 33); AtomicReference<User> atomicReference = new AtomicReference<User>(); atomicReference.set(z3); //期望是z3,如果正确,则修改为li4 System.out.println(atomicReference.compareAndSet(z3, li4)+"\t"+atomicReference.get().toString()); System.out.println(atomicReference.compareAndSet(z3, li4)+"\t"+atomicReference.get().toString()); } } class User{ String name; int age; public User(String name, int age) { super(); this.name = name; this.age = age; } @Override public String toString() { return "User [name=" + name + ", age=" + age + "]"; } }
执行结果为:
-
代码重现ABA问题
package com.jiguiquan.www; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; /** ** 重现ABA问题,并使用AtomicStampInteger解决ABA问题 * @author jiguiquan * */ public class ABADemo { //这是普通的原子引用 static AtomicReference<Integer> atomicReference = new AtomicReference<Integer>(100); public static void main(String[] args) { new Thread(() -> { atomicReference.compareAndSet(100, 101); atomicReference.compareAndSet(101, 100); },"t1").start(); new Thread(() -> { try { //让t2线程sleep一秒钟是为了确保t1线程可以完成一次ABA操作 TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(atomicReference.compareAndSet(100, 666)+"\t 最新值为:"+atomicReference.get()); },"t2").start(); } }
执行结果如下:
显然,线程t2根本就不知道线程t1已经执行了一次ABA操作;
-
使用AtomicStampedReference时间戳原子引用解决ABA问题
所谓的AtomicStampedReference,就是指在每一次操作的时候,都增加一次版本号,或者称为时间戳
package com.jiguiquan.www; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicStampedReference; /** ** 重现ABA问题,并使用AtomicStampedInteger解决ABA问题 * @author jiguiquan * */ public class ABADemo { //这是普通的原子引用 static AtomicReference<Integer> atomicReference = new AtomicReference<Integer>(100); //这是带版本号(时间戳)的原子引用,初始化的时候,除了初始化值外,还要初始化一个版本号 static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(100, 1); public static void main(String[] args) { System.out.println("==============以下是ABA问题的产生==============="); new Thread(() -> { atomicReference.compareAndSet(100, 101); atomicReference.compareAndSet(101, 100); },"t1").start(); new Thread(() -> { //让t2线程sleep一秒钟是为了确保t1线程可以完成一次ABA操作 try { TimeUnit.SECONDS.sleep(1); } catch ( InterruptedException e) { e.printStackTrace(); } System.out.println(atomicReference.compareAndSet(100, 666)+"\t 最新值为:"+atomicReference.get()); },"t2").start(); try { TimeUnit.SECONDS.sleep(2); } catch ( InterruptedException e) { e.printStackTrace(); } System.out.println("============以下是ABA问题的解决办法============="); new Thread(() -> { int stamp = atomicStampedReference.getStamp(); System.out.println(Thread.currentThread().getName()+"\t 第一次版本号:"+ stamp); //暂停1秒钟,是为了t4线程可以拿到开始的版本号 try { TimeUnit.SECONDS.sleep(1); } catch ( InterruptedException e) { e.printStackTrace(); } //执行ABA操作 atomicStampedReference.compareAndSet(100, 101, stamp, atomicStampedReference.getStamp()+1); System.out.println(Thread.currentThread().getName()+"\t 第二次的版本号:"+atomicStampedReference.getStamp()); atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1); System.out.println(Thread.currentThread().getName()+"\t 第三次的版本号:"+atomicStampedReference.getStamp()); },"t3").start(); new Thread(() -> { int stamp = atomicStampedReference.getStamp(); System.out.println(Thread.currentThread().getName()+"\t 第一次的版本号:"+stamp); //这里暂停3秒钟,是为了让t3线程完成一次ABA操作; try { TimeUnit.SECONDS.sleep(3); } catch ( InterruptedException e) { e.printStackTrace(); } boolean flag = atomicStampedReference.compareAndSet(100, 666, stamp, atomicStampedReference.getStamp()+1); System.out.println(Thread.currentThread().getName()+"\t 修改成功否:"+flag+"\t 此时版本号:"+atomicStampedReference.getStamp()); System.out.println(Thread.currentThread().getName()+"\t 当前最新值:"+atomicStampedReference.getReference()); },"t4").start(); } }
运行结果为:
结论:
经过了t3的ABA操作,在t4执行compareAndSet的时候,即使值为100,符合预期,但是由于版本号不为t4之前拿到的版本号1,此时版本号为3,所以t4的操作无法成功;
ABA问题就这样得到了解决;