谈谈对ABA问题的理解

从一个AtomicInteger原子类,我们可以引申出以下一系列知识点,可以打包一起记忆:

image.png

一、什么是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 + "]";
	}
}

执行结果为:

image.png

  • 代码重现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();
	}
}

执行结果如下:

image.png

显然,线程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();
	}
}

运行结果为:

image.png

结论:

经过了t3的ABA操作,在t4执行compareAndSet的时候,即使值为100,符合预期,但是由于版本号不为t4之前拿到的版本号1,此时版本号为3,所以t4的操作无法成功;

ABA问题就这样得到了解决;






jiguiquan@163.com

文章作者信息...

留下你的评论

*评论支持代码高亮<pre class="prettyprint linenums">代码</pre>

相关推荐