问:谈一下原子类AtomicInteger的ABA问题?是否知道什么是原子更新引用?如何解决ABA问题
答:
1、CAS会导致“ABA问题”。
CAS算法实现的一个重要前提,是取出内存中某时刻的数据,然后比较并交换。在多线程情况下,就存在取出数据后,该数据被其他线程修改的情况。
1)比如线程1从主内存中取出的数据是A,然后进行一些业务操作(比如需要5s),最后并准备将该数据改为C;
2)此时线程2抢占到cpu资源,然后进行一些业务操作(比如需要2s),然后将该共享数据改为B;
3)此时线程1还在进行业务操作,因此线程3抢占到cpu资源,然后进行一些业务操作(比如需要2s),然后将该共享数据改为A;
此时,线程1的业务操作结束了,当它来进行cas时,发现该数据是A,比较并交换后,改为了C。
上面发生的步骤,就可以理解为CAS的ABA问题。
说明一下,如果只要求该数据最终结果是A,不关心中间是否有其他改动,那么该问题影响不大,或者说没有影响。
但是,举个例子,该数据是用户的钱,或者说是仓库的货物量。每一次操作应该都是受监督的,不可能说有人挪用了钱,或者挪用货物去卖了。当有人来查的时候,又将数据加了回去。虽然最终的数据是对的上的,但是肯定这样肯定是存在问题的。
我们要有技术能解决上面的问题。
2、除了ABA问题,在多线程情况下,之前解决n++的原子性问题,使用的是AtomicInteger类。那么如果要解决的是某一个自定义类的原子性问题呢?此时就可以使用原子更新引用:AtomicReference
3、要解决ABA问题,可以使用带时间戳的原子引用:AtomicStampedReference
要很好地理解本文提到的ABA问题,首先要充分理解CAS的概念和作用,不清楚的话,可以先看下这篇博文(Java基础:CAS详解)。
1、通过原子引用代码验证ABA问题
在前两篇博文中,说明原子性的时候,都是以n++进行举例的。
但是实际开发中,肯定还有很多自定义的类,此时也需要进行原子引用。那么就可以使用并发包下的原子引用:AtomicReference。
首先创建一个仓库货物类:WareHouseGoods。
package com.koping.test;
public class WareHouseGoods {
public String name;
public String type;
public int number;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
public WareHouseGoods(String name, String type, int number) {
this.name = name;
this.type = type;
this.number = number;
}
@Override
public String toString() {
return "WareHouseGoods{" +
"name='" + name + '\'' +
", type='" + type + '\'' +
", number=" + number +
'}';
}
}
然后通过代码来验证下ABA问题,验证代码如下,运行结果如下图。
可以看到,在线程1处理逻辑时(那5s内),线程2自己销售了,然后又拿了一些货物回到仓库。
此时线程1发现仓库还是100件时,就直接取出并销售了。
丝毫不知道中间发生了ABA问题,有人拿了你的货物中间高卖低买可能已经赚过钱了,也可能以次充好给你放到仓库了。这些线程1居然都不知道,居然成功了。
package com.koping.test;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
public class AbaDemo {
static WareHouseGoods wareHouseGoods1 = new WareHouseGoods("裙子", "女性", 100);
static WareHouseGoods wareHouseGoods2 = new WareHouseGoods("裙子", "女性", 80);
static WareHouseGoods wareHouseGoods3 = new WareHouseGoods("裙子", "女性", 50);
static AtomicReference<WareHouseGoods> atomicReference = new AtomicReference<>(wareHouseGoods1);
public static void main(String[] args) {
// 线程1需要取50件货物进行销售,剩余50件。但是在之前,线程1会先做一些业务操作,假设5s.
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(5);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("线程1是否成功拿出50件: " + atomicReference.compareAndSet(wareHouseGoods1, wareHouseGoods3));
System.out.println("现在仓库货物的总数为:" + atomicReference.get());
},"线程1").start();
new Thread(() -> {
// 当前程1做业务逻辑操作时,线程2可以先取出20件进行销售,剩余80件;
// 然后再自己补回20件到仓库中,因此剩余还是100件。
atomicReference.compareAndSet(wareHouseGoods1, wareHouseGoods2);
System.out.println("线程2已拿出20件进行销售,现在仓库货物的总数为:" + atomicReference.get());
atomicReference.compareAndSet(wareHouseGoods2, wareHouseGoods1);
System.out.println("线程2已放回20件到仓库中,现在仓库货物的总数为:" + atomicReference.get());
},"线程2").start();
}
}
2、通过带时间戳的原子引用解决ABA问题
要解决上一小节中的ABA问题,可以时间带时间戳的原子引用:AtomicStampedReference。
1)每一批货物都是有版本号的,货物有初始版本号:1。
2)当线程1从数据库获取的版本号是1,它就还是去做业务操作了;
3)此时线程2私自将货物进行了取出和放回,虽然最终货物还是100件,单数版本号已经变为了3;
运行结果如下图,可以看到此时线程1获取50件货物的时候就失败了,因为发现版本号不是之前的版本号1了。这时候系统就知道有人动过仓库的货物了,因此可以进行追查。
package com.koping.test;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
public class AbaDemo {
static WareHouseGoods wareHouseGoods1 = new WareHouseGoods("裙子", "女性", 100);
static WareHouseGoods wareHouseGoods2 = new WareHouseGoods("裙子", "女性", 80);
static WareHouseGoods wareHouseGoods3 = new WareHouseGoods("裙子", "女性", 50);
// 初始版本号是1
static AtomicStampedReference<WareHouseGoods> atomicReference = new AtomicStampedReference<>(wareHouseGoods1, 1);
public static void main(String[] args) {
// 线程1需要取50件货物进行销售,剩余50件。但是在之前,线程1会先做一些业务操作,假设5s.
new Thread(() -> {
// 先从数据库中获取之前的版本号,再进行业务操作
int stamp = atomicReference.getStamp();
try {
TimeUnit.SECONDS.sleep(5);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("\n线程1是否成功拿出50件: " + atomicReference.compareAndSet(wareHouseGoods1, wareHouseGoods3, stamp, stamp+1));
System.out.println("现在仓库货物的总数为:" + atomicReference.getReference());
},"线程1").start();
new Thread(() -> {
// 当前程1做业务逻辑操作时,线程2可以先取出20件进行销售,剩余80件;
// 然后再自己补回20件到仓库中,因此剩余还是100件。
atomicReference.compareAndSet(wareHouseGoods1, wareHouseGoods2, atomicReference.getStamp(), atomicReference.getStamp()+1);
System.out.println("线程2已拿出20件进行销售,现在仓库货物的总数为:" + atomicReference.getReference());
System.out.println("此时仓库货物的版本号为: " + atomicReference.getStamp());
atomicReference.compareAndSet(wareHouseGoods2, wareHouseGoods1, atomicReference.getStamp(), atomicReference.getStamp()+1);
System.out.println("线程2已放回20件到仓库中,现在仓库货物的总数为:" + atomicReference.getReference());
System.out.println("此时仓库货物的版本号为: " + atomicReference.getStamp());
},"线程2").start();
}
}