在上一節java開發:樂觀鎖CAS機制中我們說過CAS機制的原理,與及使用CAS會發生的ABA問題解決辦法。
java中提供一些列的基本數據類型原子操作類用來實現操作基本數據類型時保證線程安全,其底層就是使用CAS機制實現的。AtomicInteger
則是用來操作int類型的數據,保證線程安全。
什麼是ABA問題呢?舉個例子:比如說我的卡里有100大洋,此時有倆個線程同時去操作這100大洋,它們拿到的副本都是100。若線程2被阻塞了,線程1執行任務扣掉了50大洋,現在卡里剩50。正好我媽(線程3)此時剛好打給我50大洋,現在卡里的錢又變回了100。當線程2被喚醒後也執行扣款操作,扣掉50大洋,當它準備提交數據時比較舊的預期值和共享內存的實際值發現都是100,因此線程2也扣款成功,卡里只剩50大洋。。。想想就不對勁啊,我本來有100,我媽打給我50,我就取了50。不應該是剩100嗎?這不害我白白損失了50大洋嗎
final AtomicInteger atomicInteger = new AtomicInteger(100);
ExecutorService service = Executors.newCachedThreadPool();
service.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
//獲取最新的值
int result = atomicInteger.get();
System.out.println("線程1預期值"+result);
//休眠五秒
Thread.sleep(5000);
//修改共享變量,減去50
atomicInteger.addAndGet(-50);
System.out.println("線程1新值"+atomicInteger.get() );
return null;
}
});
service.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
//獲取最新的值
int result = atomicInteger.get();
System.out.println("線程2預期值"+result);
//修改共享變量,減去50
atomicInteger.addAndGet(-50);
return null;
}
});
service.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
//休眠1秒
Thread.sleep(1000);
//獲取最新值
int result = atomicInteger.get();
System.out.println("線程3預期值"+result);
atomicInteger.addAndGet(50);
return null;
}
});
2019-12-27 21:26:44.691 14154-14189/? I/System.out: 線程1預期值100
2019-12-27 21:26:44.693 14154-14190/? I/System.out: 線程2預期值100
2019-12-27 21:26:45.694 14154-14191/com.example.serializationapplication I/System.out: 線程3預期值50
`2019-12-27 21:26:49.693 14154-14189/? I/System.out: 線程1新值50
上訴代碼就是一個典型的ABA問題,暫且不管AtomicInteger 底層是怎麼實現CAS的。
我們的初始值爲100
,線程1和線程2可以說是同時獲取atomicInteger
對象的值都是100
,線程1獲取完後進入休眠狀態。此時線程2執行任務扣去了50
,共享變量成了50
,然後線程3休眠結束,獲取atomicInteger
對象的值的得到50
,線程3執行任務又把共享變量改成了100
。等待線程1休眠結束後,執行任務扣去50
,在提交之前它判斷舊的預期值和共享內存的實際值是否相等,倆者得到的都是100
,因此線程1認爲在它休眠過程中這個變量沒有被修改,則它扣款成功,共享變量變爲50
。這就CAS
造成的ABA
問題。而在上一篇文章我們也說到解決ABA問題就是給共享變量加上版本號,而java中則提供AtomicStampedReference
幫助我們實現了。把上訴的代碼修改:
final AtomicStampedReference reference =new AtomicStampedReference(100,0);
ExecutorService service = Executors.newCachedThreadPool();
service.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
//獲取舊值
int result = (int) reference.getReference();
System.out.println("線程1的預期值"+result);
//休眠五秒
Thread.sleep(5000);
//參數:舊的預期值、新值、舊的版本號、新版本號
reference.compareAndSet(reference.getReference(),50,reference.getStamp(),reference.getStamp()+1);
System.out.println("線程1的新值"+atomicInteger.get());
return null;
}
});
Future future = service.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
int result = (int) reference.getReference();
System.out.println("線程2的預期值"+result);
reference.compareAndSet(reference.getReference(),50,reference.getStamp(),reference.getStamp()+1);
return null;
}
});
service.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
Thread.sleep(1000);
int result = (int) reference.getReference();
System.out.println("線程3的預期值"+result);
reference.compareAndSet(reference.getReference(),100,reference.getStamp(),reference.getStamp()+1);
return null;
}
});
2019-12-27 21:46:42.572 15859-15905/com.example.serializationapplication I/System.out: 線程1的預期值100
2019-12-27 21:46:42.573 15859-15906/com.example.serializationapplication I/System.out: 線程2的預期值100
2019-12-27 21:46:43.582 15859-15907/com.example.serializationapplication I/System.out: 線程3的預期值50
2019-12-27 21:46:47.573 15859-15905/com.example.serializationapplication I/System.out: 線程1的新值100
AtomicStampedReference
代替了AtomicInteger
,AtomicStampedReference
的構造函數多了一個版本號,getReference()
方法我們可以獲取當前的值,getStamp()
可以獲取當前的版本號。可以看到我們輸出的結果解決了ABA問題。