聲明:本文爲作者原創,如若轉發,請指明轉發地址
1、CAS是什麼?
interface Account {
// 獲取餘額
Integer getBalance();
// 取款
void withdraw(Integer amount);
/**
* 方法內會啓動 1000 個線程,每個線程做 -10 元 的操作
* 如果初始餘額爲 10000 那麼正確的結果應當是 0
*/
static void demo(Account account) {
List<Thread> ts = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
ts.add(new Thread(() -> {
account.withdraw(10);
}));
}
long start = System.nanoTime();
ts.forEach(Thread::start);
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.nanoTime();
System.out.println(account.getBalance()
+ " cost: " + (end - start) / 1000_000 + " ms");
}
}
//下面的方法時線程不安全的
class AccountUnsafe implements Account {
private Integer balance;
public AccountUnsafe(Integer balance) {
this.balance = balance;
}
@Override
public Integer getBalance() {
return this.balance;
}
@Override
public void withdraw(Integer amount) {
this.balance -= amount;
}
}
//測試類
public class TestAccount {
public static void main(String[] args) {
Account account = new AccountUnsafe(10000);
Account.demo(account);
}
}
1、解決方法:使用synchronized解決線程安全問題
//對成員變量使用同步保證線程安全
class AccountUnsafe implements Account {
private Integer balance;
public AccountUnsafe(Integer balance) {
this.balance = balance;
}
@Override
public Integer getBalance() {
synchronized (this) {
return this.balance;
}
}
@Override
public void withdraw(Integer amount) {
synchronized (this) {
this.balance -= amount;
}
}
}
2、解決方法:使用無鎖CAS解決線程安全問題
//使用無鎖的方式也能保證線程安全
class AccountCas implements Account {
private AtomicInteger balance;
public AccountCas(int balance) {
this.balance = new AtomicInteger(balance);
}
@Override
public Integer getBalance() {
return balance.get();
}
@Override
public void withdraw(Integer amount) {
while (true) {
// 獲取餘額的最新值
int prev = balance.get();
// 修改餘額
int next = prev - amount;
// 真正修改
/*
compareAndSet 正是做這個檢查,在 set 前,先比較 prev 與當前值
- 不一致了,next 作廢,返回 false 表示失敗
比如,別的線程已經做了減法,當前值已經被減成了 990
那麼本線程的這次 990 就作廢了,進入 while 下次循環重試
- 一致,以 next 設置爲新值,返回 true 表示成功
*/
if (balance.compareAndSet(prev, next)) {
break;
}
}
}
}
其中的關鍵是 compareAndSet,它的簡稱就是 CAS (也有 Compare And Swap 的說法),它必須是原子操作。
CAS有三個操作數,舊值prev,主存中的新值,要更改成的新值next。當且僅當舊值prev和主存中的新值一致時,纔會將主存中的值更改爲新值next,否則什麼也不做。
上面的流程圖:線程1和線程2同時更新同一變量Account對象的值
(1) 線程1和線程2從主存中讀取Account=100到自己的工作內存中
(2) 線程1將Account=100修改爲90,並將結果同步到主存中(寫屏障的原因),因此此時主存中Account=90
(3) 線程2向要通過CAS操作將Account變量的值修改爲90,於是在set之前就會重新讀取主存的值(讀屏障的原因)並與工作內存中的值進行比較,如果相同就說明沒有其他線程更改共享數據,成功的將主存中的值修改爲90,但是如果不一致就說明CAS失敗,此時就會進行下一次CAS操作(CAS自旋,前提在while循環內)
具體執行流程圖:
2、CAS需要volatile的支持
獲取共享變量時,爲了保證該變量的可見性,需要使用 volatile 修飾。它可以用來修飾成員變量和靜態成員變量,他可以避免線程從自己的工作緩存中查找變量的值,必須到主存中獲取它的值,線程操作 volatile 變量都是直接操作主存。即一個線程對 volatile 變量的修改,對另一個線程可見。
注意:
volatile 僅僅保證了共享變量的可見性,讓其它線程能夠看到最新值,但不能解決指令交錯問題(不能保證原
子性)
CAS 必須藉助 volatile 才能讀取到共享變量的最新值來實現【比較並交換】的效果
public class AtomicInteger extends Number implements java.io.Serializable {
private volatile int value;
public AtomicInteger(int initialValue) {
value = initialValue;
}
}
3、爲什麼CAS+volatile效率更高?
無鎖情況下,即使重試失敗,線程始終在高速運行,沒有停歇,而 synchronized 會讓線程在沒有獲得鎖的時
候,發生上下文切換,進入阻塞。
打個比喻:
線程就好像高速跑道上的賽車,高速運行時,速度超快,一旦發生上下文切換,就好比賽車要減速、熄火,
等被喚醒又得重新打火、啓動、加速… 恢復到高速運行,代價比較大
但無鎖情況下,因爲線程要保持運行,需要額外 CPU 的支持,CPU 在這裏就好比高速跑道,沒有額外的跑
道,線程想高速運行也無從談起,雖然不會進入阻塞,但由於沒有分到時間片,仍然會進入可運行狀態,還
是會導致上下文切換。
結合 CAS + volatile 可以實現無鎖併發,適用於線程數少、多核 CPU 的場景下。
CAS 是基於樂觀鎖的思想:最樂觀的估計,不怕別的線程來修改共享變量,就算改了也沒關係,我喫虧點再
重試唄。
synchronized 是基於悲觀鎖的思想:最悲觀的估計,得防着其它線程來修改共享變量,我上了鎖你們都別想
改,我改完了解開鎖,你們纔有機會。
CAS 體現的是無鎖併發、無阻塞併發,請仔細體會這兩句話的意思。因爲沒有使用 synchronized,所以線程不會陷入阻塞,這是效率提升的因素之一,但如果競爭激烈,可以想到重試必然頻繁發生,反而效率會受影響。
CAS缺點:
- CPU開銷較大,多線程反覆嘗試更新某一個變量的時候容易出現;
- 不能保證代碼塊的原子性,只能保證變量的原子性操作;
- ABA問題
4、AtomicInteger?
注意:如果是這個問題,就是在問你CAS,你只需要將CAS講一遍,然後再加上本類的特點即可。
class AccountCas implements Account {
private AtomicInteger balance;
public AccountCas(int balance) { this.balance = new AtomicInteger(balance);}
@Override
public Integer getBalance() {return balance.get();}
@Override
public void withdraw(Integer amount) {
while (true) {
int prev = balance.get();
int next = prev - amount;
// 真正修改(因爲該變量balance是AtomicInteger類型的,因此可以調用該方法)
if (balance.compareAndSet(prev, next)) {
break;
}
}
}
}
除了AtomicInteger類中的CAS方法外,還有其他封住的一些比較方便的方法(CAS需要放在while循環中):
AtomicInteger i = new AtomicInteger(0);
// 獲取並自增(i = 0, 結果 i = 1, 返回 0),類似於 i++
System.out.println(i.getAndIncrement());
// 自增並獲取(i = 1, 結果 i = 2, 返回 2),類似於 ++i
System.out.println(i.incrementAndGet());
// 自減並獲取(i = 2, 結果 i = 1, 返回 1),類似於 --i
System.out.println(i.decrementAndGet());
// 獲取並自減(i = 1, 結果 i = 0, 返回 1),類似於 i--
System.out.println(i.getAndDecrement());
// 獲取並加值(i = 0, 結果 i = 5, 返回 0)
System.out.println(i.getAndAdd(5));
// 加值並獲取(i = 5, 結果 i = 0, 返回 0)
System.out.println(i.addAndGet(-5));
// 獲取並更新(i = 0, p 爲 i 的當前值, 結果 i = -2, 返回 0)
// 其中函數中的操作能保證原子,但函數需要無副作用
System.out.println(i.getAndUpdate(p -> p - 2));
// 更新並獲取(i = -2, p 爲 i 的當前值, 結果 i = 0, 返回 0)
// 其中函數中的操作能保證原子,但函數需要無副作用
System.out.println(i.updateAndGet(p -> p + 2));
將上面的CAS操作改成下面的方法會簡便很多:不需要使用while循環,並且簡化了操作步驟
class AccountCas implements Account {
private AtomicInteger balance;
public AccountCas(int balance) { this.balance = new AtomicInteger(balance);}
@Override
public Integer getBalance() {return balance.get();}
@Override
public void withdraw(Integer amount) {
balance.getAndAdd(-1*amount)
}
}
5、CAS和ABA的問題如何解決?
AtomicReference(原子引用)
AtomicMarkableReference
AtomicStampedReference
class DecimalAccountCas implements DecimalAccount {
private AtomicReference<BigDecimal> balance;
public DecimalAccountCas(BigDecimal balance) {
this.balance = new AtomicReference<>(balance);
}
//獲取餘額
@Override
public BigDecimal getBalance() {
return balance.get();
}
@Override
public void withdraw(BigDecimal amount) {
while(true) {
BigDecimal prev = balance.get();
BigDecimal next = prev.subtract(amount);
if (balance.compareAndSet(prev, next)) {
break;
}
}
}
}
1、什麼是ABA問題?
因爲CAS需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了
@Slf4j(topic = "c.TestCAS")
public class TestCAS {
static AtomicReference<String> ref = new AtomicReference<>("A");
public static void main(String[] args) throws InterruptedException {
log.debug("main start...");
//獲取共享變量的舊值A
String prev = ref.get();
//調用other()方法
other();
Thread.sleep(1000);
// CAS操作將A改爲C
log.debug("change A->C {}", ref.compareAndSet(prev, "C"));
}
//其他線程將共享變量從A改成B,又從B改成A,但主線程感知不到,主線程只會判斷最新獲取到的值A與prev是否相同
private static void other() throws InterruptedException {
new Thread(() -> {
//線程t1將A改成B
log.debug("change A->B {}", ref.compareAndSet(ref.get(), "B"));
}, "t1").start();
Thread.sleep(500);
new Thread(() -> {
//線程t2將B改成A
log.debug("change B->A {}", ref.compareAndSet(ref.get(), "A"));
}, "t2").start();
}
}
執行結果:
14:05:10.388 c.TestCAS [main] - main start...
14:05:10.544 c.TestCAS [t1] - change A->B true
14:05:11.049 c.TestCAS [t2] - change B->A true
14:05:12.047 c.TestCAS [main] - change A->C true
如圖所示:
(1) t1線程讀取主存中的Ref引用變量的值A到自己的工作內存中,將其從A改爲了B,並同步到了主存
(2) t2線程讀取主存中的Ref引用變量的值B到自己的工作內存中,將其從B改成了A,並同步到了主存
(3) 此時主存中的值經歷了A----》B---》A
的過程,但是主線程感知不到
(4) 主線程進行CAS操作將Ref變量的值更改爲了C(將工作線程中的值和主存中的值進行比較,發現一致,就認爲主存中的共享變量的值沒有更改過)
主線程僅能判斷出共享變量的值與最初值 A 是否相同,不能感知到這種從 A 改爲 B 又 改回 A 的情況,如果主線程
希望:只要有其它線程【動過了】共享變量,那麼自己的 cas 就算失敗,這時,僅比較值是不夠的,需要再加一個版本號 。
2、AtomicStampedReference
在變量前面追加上版本號,每次變量更新的時候把版本號加一,那麼A-B-A 就會變成1A-2B-3A。 從Java1.5開始JDK的atomic包裏提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法作用是首先檢查當前引用是否等於預期引用,並且當前標誌是否等於預期標誌,如果全部相等,則以原子方式將該引用和該標誌的值設置爲給定的更新值。
@Slf4j(topic = "c.TestCAS1")
public class TestCAS1 {
//原子引用變量的初始值爲A
static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
public static void main(String[] args) throws InterruptedException {
log.debug("main start...");
//獲取舊值A
String prev = ref.getReference();
// 獲取版本號
int stamp = ref.getStamp();
log.debug("版本 {}", stamp);
// 如果中間有其它線程干擾,發生了 ABA 現象
other();
Thread.sleep(1000);
// 主線程嘗試改爲 C
log.debug("change A->C {}", ref.compareAndSet(prev, "C", stamp, stamp + 1));
}
private static void other() throws InterruptedException {
//每次更新成功後,將版本號加1
new Thread(() -> {
log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B", ref.getStamp(), ref.getStamp() + 1));
log.debug("更新版本爲 {}", ref.getStamp());
}, "t1").start();
Thread.sleep(500);
//每次更新成功後,將版本號加1
new Thread(() -> {
log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A", ref.getStamp(), ref.getStamp() + 1));
log.debug("更新版本爲 {}", ref.getStamp());
}, "t2").start();
}
}
執行結果:
14:04:28.851 c.TestCAS1 [main] - main start...
14:04:28.856 c.TestCAS1 [main] - 版本 0
14:04:28.977 c.TestCAS1 [t1] - change A->B true
14:04:28.977 c.TestCAS1 [t1] - 更新版本爲 1
14:04:29.482 c.TestCAS1 [t2] - change B->A true
14:04:29.483 c.TestCAS1 [t2] - 更新版本爲 2
14:04:30.477 c.TestCAS1 [main] - change A->C false
(1) 線程1從主存中將Ref
引用變量的值和Stamp
版本號的值讀入到工作內存中,並將Ref從A改成了B,Stamp從0改成了1
(2) 線程1從主存中將Ref
引用變量的值和Stamp
版本號的值讀入到工作內存中,並將Ref從B改成了A,Stamp從1改成了2
(3) 此時主存中的Ref引用變量的值爲A,Stamp版本號的值爲2
(4) 主線程在進行CAS操作時,會將主存中的Ref變量的值A(新值)和工作內存中Ref變量的值A(舊值)進行比較
,判斷是否相同(相同),同時還會將主存中Stamp版本號的值2(新值)和工作內存中Stamp版本號的值0(舊值)進行比較
,判斷是否相同(不同),只有兩者都相同,CAS纔會成功。
AtomicStampedReference 可以給原子引用加上版本號,追蹤原子引用整個的變化過程,如: A -> B -> A ->
C ,通過AtomicStampedReference,我們可以知道,引用變量中途被更改了幾次。
但是有時候,並不關心引用變量更改了幾次,只是單純的關心是否更改過。
3、AtomicMarkableReference
AtomicMarkableReference可以理解爲上面AtomicStampedReference的簡化版,就是不關心修改過幾次,僅僅關心是否修改過。因此變量mark是boolean類型,僅記錄值是否有過修改。
@Slf4j(topic = "c.Test38")
public class Test38 {
public static void main(String[] args) throws InterruptedException {
GarbageBag bag = new GarbageBag("裝滿了垃圾");
// 參數2 mark 可以看作一個標記,表示垃圾袋滿了
AtomicMarkableReference<GarbageBag> ref = new AtomicMarkableReference<>(bag, true);
log.debug("start...");
//獲取垃圾袋
GarbageBag prev = ref.getReference();
log.debug(prev.toString());
new Thread(() -> {
log.debug("start...");
bag.setDesc("空垃圾袋");
//保潔阿姨將垃圾桶的垃圾倒空,並將垃圾狀態從true(滿)改爲false(空)
ref.compareAndSet(bag, bag, true, false);
log.debug(bag.toString());
},"保潔阿姨").start();
sleep(1);
log.debug("想換一隻新垃圾袋?");
//房東想要更換垃圾袋,發現垃等換失敗,因爲垃圾袋狀態爲false(空),但是期待的爲true(滿)
boolean success = ref.compareAndSet(prev, new GarbageBag("空垃圾袋"), true, false);
log.debug("換了麼?" + success);
log.debug(ref.getReference().toString());
}
}
class GarbageBag {
String desc;
public GarbageBag(String desc) {
this.desc = desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
@Override
public String toString() {
return super.toString() + " " + desc;
}
}
執行結果:
14:43:55.104 c.Test38 [main] - start...
14:43:55.113 c.Test38 [main] - cn.itcast.test.GarbageBag@769c9116 裝滿了垃圾
14:43:55.254 c.Test38 [保潔阿姨] - start...
14:43:55.254 c.Test38 [保潔阿姨] - cn.itcast.test.GarbageBag@769c9116 空垃圾袋
14:43:56.277 c.Test38 [main] - 想換一隻新垃圾袋?
14:43:56.277 c.Test38 [main] - 換了麼?false
14:43:56.277 c.Test38 [main] - cn.itcast.test.GarbageBag@769c9116 空垃圾袋
(1) 保潔阿姨線程從主存中讀取bag變量到工作內存中,並將當前垃圾袋倒空,但是並沒有更換垃圾袋同時將flag的狀態從true更改爲false
然後同步到主存中。
(2) 此時主存中的bag還是原來的bag(還是原來的垃圾袋),標記變量boolean flag = false
(3) 此時房東線程想要通過CAS更換垃圾袋,首先將工作內存中的垃圾袋bag和主存中的垃圾袋bag進行比較
,判斷是否相同(相同),然後將工作內存中的flag=true與主存中的flag=false
進行比較,判斷是否相同(不同),自由兩個都相同,CAS才能成功。