大家可能都聽說說 Java 中的并發(fā)包,如果想要讀懂 Java 中的并發(fā)包,其核心就是要先讀懂 CAS 機(jī)制,因為 CAS 可以說是并發(fā)包的底層實(shí)現(xiàn)原理。
今天就帶大家讀懂 CAS 是如何保證操作的原子性的,以及 Java8 對 CAS 進(jìn)行了哪些優(yōu)化。
synchronized:大材小用
我們先來看幾行代碼:
public class CASTest {
static int i = 0;
public static void increment() {
i++;
}
}
假如有100個線程同時調(diào)用 increment() 方法對 i 進(jìn)行自增操作,i 的結(jié)果會是 100 嗎?
學(xué)會多線程的同學(xué)應(yīng)該都知道,這個方法是線程不安全的,由于 i++ 不是一個原子操作,所以是很難得到 100 的。
這里稍微解釋下為啥會得不到 100(知道的可直接跳過), i++ 這個操作,計算機(jī)需要分成三步來執(zhí)行。
1、讀取 i 的值。
2、把 i 加 1.
3、把 最終 i 的結(jié)果寫入內(nèi)存之中。所以,假如線程 A 讀取了 i 的值為 i = 0,這個時候線程 B 也讀取了 i 的值 i = 0。接著 A把 i 加 1,然后寫入內(nèi)存,此時 i = 1。緊接著,B也把 i 加 1,此時線程B中的 i = 1,然后線程 B 把 i 寫入內(nèi)存,此時內(nèi)存中的 i = 1。也就是說,線程 A, B 都對 i 進(jìn)行了自增,但最終的結(jié)果卻是 1,不是 2.
那該怎么辦呢?解決的策略一般都是給這個方法加個鎖,如下
public class CASTest {
static int i = 0;
public synchronized static void increment() {
i++;
}
}
加了 synchronized 之后,就最多只能有一個線程能夠進(jìn)入這個 increment() 方法了。這樣,就不會出現(xiàn)線程不安全了。
然而,一個簡簡單單的自增操作,就加了 synchronized 進(jìn)行同步,好像有點(diǎn)大材小用的感覺,加了 synchronized 關(guān)鍵詞之后,當(dāng)有很多線程去競爭 increment 這個方法的時候,拿不到鎖的方法是會被阻塞在方法外面的,最后再來喚醒他們,而阻塞/喚醒這些操作,是非常消耗時間的。
這里可能有人會說,synchronized 到了JDK1.6之后不是做了很多優(yōu)化嗎?是的,確實(shí)做了很多優(yōu)化,增加了偏向鎖、輕量級鎖等, 但是,就算增加了這些,當(dāng)很多線程來競爭的時候,開銷依然很多,
CAS :這種小事交給我
那有沒有其他方法來代替 synchronized 對方法的加鎖,并且保證 increment() 方法是線程安全呢?
大家看一下,如果我采用下面這種方式,能否保證 increment 是線程安全的呢?步驟如下:
1、線程從內(nèi)存中讀取 i 的值,假如此時 i 的值為 0,我們把這個值稱為 k 吧,即此時 k = 0。
2、令 j = k + 1。
3、用 k 的值與內(nèi)存中i的值相比,如果相等,這意味著沒有其他線程修改過 i 的值,我們就把 j(此時為1) 的值寫入內(nèi)存;如果不相等(意味著i的值被其他線程修改過),我們就不把j的值寫入內(nèi)存,而是重新跳回步驟 1,繼續(xù)這三個操作。
翻譯成代碼的話就是這樣:
public static void increment() {
do{
int k = i;
int j = k + 1;
}while (compareAndSet(i, k, j))
}
如果你去模擬一下,就會發(fā)現(xiàn),這樣寫是線程安全的。
這里可能有人會說,第三步的 compareAndSet 這個操作不僅要讀取內(nèi)存,還干了比較、寫入內(nèi)存等操作,,,這一步本身就是線程不安全的啊?
如果你能想到這個,說明你是真的有去思考、模擬這個過程,不過我想要告訴你的是,這個 compareAndSet 操作,他其實(shí)只對應(yīng)操作系統(tǒng)的一條硬件操作指令,盡管看似有很多操作在里面,但操作系統(tǒng)能夠保證他是原子執(zhí)行的。
對于一條英文單詞很長的指令,我們都喜歡用它的簡稱來稱呼他,所以,我們就把 compareAndSet 稱為 CAS 吧。
所以,采用 CAS 這種機(jī)制的寫法也是線程安全的,通過這種方式,可以說是不存在鎖的競爭,也不存在阻塞等事情的發(fā)生,可以讓程序執(zhí)行的更好。
在 Java 中,也是提供了這種 CAS 的原子類,例如:
- AtomicBoolean
- AtomicInteger
- AtomicLong
- AtomicReference
具體如何使用呢?我就以上面那個例子進(jìn)行改版吧,代碼如下:
public class CASTest {
static AtomicInteger i = new AtomicInteger(0);
public static void increment() {
// 自增 1并返回之后的結(jié)果
i.incrementAndGet();
}
}
CAS:誰偷偷更改了我的值
雖然這種 CAS 的機(jī)制能夠保證increment() 方法,但依然有一些問題,例如,當(dāng)線程A即將要執(zhí)行第三步的時候,線程 B 把 i 的值加1,之后又馬上把 i 的值減 1,然后,線程 A 執(zhí)行第三步,這個時候線程 A 是認(rèn)為并沒有人修改過 i 的值,因為 i 的值并沒有發(fā)生改變。而這,就是我們平常說的ABA問題。
對于基本類型的值來說,這種把數(shù)字改變了在改回原來的值是沒有太大影響的,但如果是對于引用類型的話,就會產(chǎn)生很大的影響了。
來個版本控制吧
為了解決這個 ABA 的問題,我們可以引入版本控制,例如,每次有線程修改了引用的值,就會進(jìn)行版本的更新,雖然兩個線程持有相同的引用,但他們的版本不同,這樣,我們就可以預(yù)防 ABA 問題了。Java 中提供了 AtomicStampedReference 這個類,就可以進(jìn)行版本控制了。
Java8 對 CAS 的優(yōu)化。
由于采用這種 CAS 機(jī)制是沒有對方法進(jìn)行加鎖的,所以,所有的線程都可以進(jìn)入 increment() 這個方法,假如進(jìn)入這個方法的線程太多,就會出現(xiàn)一個問題:每次有線程要執(zhí)行第三個步驟的時候,i 的值老是被修改了,所以線程又到回到第一步繼續(xù)重頭再來。
而這就會導(dǎo)致一個問題:由于線程太密集了,太多人想要修改 i 的值了,進(jìn)而大部分人都會修改不成功,白白著在那里循環(huán)消耗資源。
為了解決這個問題,Java8 引入了一個 cell[] 數(shù)組,它的工作機(jī)制是這樣的:假如有 5 個線程要對 i 進(jìn)行自增操作,由于 5 個線程的話,不是很多,起沖突的幾率較小,那就讓他們按照以往正常的那樣,采用 CAS 來自增吧。
但是,如果有 100 個線程要對 i 進(jìn)行自增操作的話,這個時候,沖突就會大大增加,系統(tǒng)就會把這些線程分配到不同的 cell 數(shù)組元素去,假如 cell[10] 有 10 個元素吧,且元素的初始化值為 0,那么系統(tǒng)就會把 100 個線程分成 10 組,每一組對 cell 數(shù)組其中的一個元素做自增操作,這樣到最后,cell 數(shù)組 10 個元素的值都為 10,系統(tǒng)在把這 10 個元素的值進(jìn)行匯總,進(jìn)而得到 100,二這,就等價于 100 個線程對 i 進(jìn)行了 100 次自增操作。
當(dāng)然,我這里只是舉個例子來說明 Java8 對 CAS 優(yōu)化的大致原理,具體的大家有興趣可以去看源碼,或者去搜索對應(yīng)的文章哦。
總結(jié)
理解 CAS 的原理還是非常重要的,它是 AQS 的基石,而 AQS 又是并發(fā)框架的基石,