以下模擬一個情景,一家商鋪要做一個活動,對那些賬戶餘額小於10的用戶,充值20元禮金,促進消費。
使用AtomicReference來實現這個邏輯的demo:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
public class AtomicReferenceTest {
//設置一個初始值,爲6
static AtomicReference<Integer> money = new AtomicReference(6);
public static void main(String[] args) {
//設置一個初始值,爲6
money.set(6);
//模擬後臺線程,這3個線程的作用就是無限循環找出賬戶金額小於10的用戶,然後對該用戶充值20元
for (int i = 0; i < 3; i++) {
new Thread(() -> {
{
while (true) {
while (true) {
Integer m = money.get();
if (m < 10) {
if (money.compareAndSet(m, m + 20)) {
System.out.println("餘額小於十元,進行充值,充值後餘額=" + money.get() + "元");
break;
}
} else {
break;
}
}
}
}
}).start();
}
//模擬用戶消費的線程,消費10次,每次只消費10元,金額不夠則給出提示
new Thread(() -> {
for (int i = 0; i < 10; i++) {
while (true) {
Integer m = money.get();
if (m > 10) {
System.out.println("賬戶餘額足夠,可以消費");
if (money.compareAndSet(m, m - 10)) {
System.out.println("消費成功,消費後餘額=" + money.get() + "元");
break;
}
} else {
System.out.println("餘額不足,餘額=" + money.get() + "元");
break;
}
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
運行結果如下:
可以發現用戶賬戶只要金額小於10,後臺線程就會給用戶賬戶充值,如此反覆,這中邏輯肯定是不符合商家利益的,這也是CAS方式會出現的ABA問題。
接下來使用AtomicStampedReference來改造以上邏輯,如下:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;
public class AtomicStampedReferenceTest {
//設置一個初始值,爲6
static AtomicStampedReference<Integer> money = new AtomicStampedReference(6, 0);
public static void main(String[] args) {
//模擬後臺線程,這3個線程的作用就是無限循環找出賬戶金額小於10的用戶,然後對該用戶充值20元
for (int i = 0; i < 3; i++) {
//取出stamp
final int stamp = money.getStamp();
new Thread(() -> {
{
while (true) {
while (true) {
Integer m = money.getReference();
if (m < 10) {
if (money.compareAndSet(m, m + 20, stamp, stamp + 1)) {
System.out.println("餘額小於十元,進行充值,充值後餘額=" + money.getReference() + "元");
break;
}
} else {
break;
}
}
}
}
}).start();
}
//模擬用戶消費的線程,消費10次,每次只消費10元,金額不夠則給出提示
new Thread(() -> {
for (int i = 0; i < 10; i++) {
while (true) {
int stamp = money.getStamp();
Integer m = money.getReference();
if (m > 10) {
System.out.println("賬戶餘額足夠,可以消費");
if (money.compareAndSet(m, m - 10, stamp, stamp + 1)) {
System.out.println("消費成功,消費後餘額=" + money.getReference() + "元");
break;
}
} else {
System.out.println("餘額不足,餘額=" + money.getReference() + "元");
break;
}
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
運行結果如下:
可以發現只充值了一次,不會再出現反覆充值的情況了。
總結
相比起AtomicReference,AtomicStampedReference除了維護了對象值,還維護了一個狀態標識位stamp,在更新對象值時,還需要同時更新stamp的值,二者合一纔是能否成功修改值的判斷規則,可以抽象的想象爲,使用了stamp屬性形成了一個對象值的修改流水歷史,這樣即使對象值被修改回原值,邏輯也不會再被運行,因爲stamp值肯定是不同的,那麼就不匹配規則了,這也是AtomicStampedReference能夠實現只充值了一次邏輯的關鍵所在,這也是對CAS操作的ABA問題的解決思路。