JMM定義了一套在多線程讀寫共享數(shù)據(jù)時(shí)(成員變量,數(shù)組)時(shí),對(duì)數(shù)據(jù)的 可見性、原子性和有序性 的規(guī)則和保障。

1 java內(nèi)存模型
1.1 原子性
Java對(duì)靜態(tài)變量的自增或者自減(i++,i--)不是原子操作。
i++的字節(jié)碼指令為:(i為靜態(tài)變量,局部變量的話不一樣)
getstatic i //獲取靜態(tài)變量i的值
iconst_1 //準(zhǔn)備常量1
iadd //自增
putstatic i //將修改后的值存入靜態(tài)變量i
i--的字節(jié)碼指令為:
getstatic i //獲取靜態(tài)變量i的值
iconst_1 //準(zhǔn)備常量1
isub //自減
putstatic i //將修改后的值存入靜態(tài)變量i
java的內(nèi)存模型如下,完成靜態(tài)變量的自增或者自減,需要在主存和線程內(nèi)存中進(jìn)行數(shù)據(jù)交換。由于多個(gè)線程按照時(shí)間片輪流使用cpu,會(huì)導(dǎo)致切換的時(shí)候值為臟值。

1.2 可見性
public class Demo4_2 {
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (run) {}
}, "t").start();
TimeUnit.SECONDS.sleep(1);
run = false;
}
}
休眠1秒后run置為false后并沒有讓線程停止,這是由于:
- 初始狀態(tài),t線程剛開始從主內(nèi)存讀取了run的值加載到工作內(nèi)存。

- 因?yàn)閠線程要頻繁從主存中讀取run的值,JIT編譯器會(huì)將run的值緩存至自己工作內(nèi)存中的高速緩存中,減少對(duì)主存中run的訪問(wèn),提高效率。

- 1秒之后,main線程修改了run值,并同步至主存,而t是從自己的工作內(nèi)存中的告訴緩存中讀取run,結(jié)果永遠(yuǎn)是舊值。

- 解決方式:
- 可以通過(guò)添加volatile(保證可見性,不保證原子性,禁止指令重排)解決。
- 在while中加入System.out.println();也能保證結(jié)束,這是由于輸出語(yǔ)句底層采用了synchronized關(guān)鍵字,保證了原子性與可見性,強(qiáng)制了t線程從主存中讀取。
1.3 有序性
主要是因?yàn)榇嬖?指令重排,同樣可以通過(guò)volatile來(lái)禁止指令重排。
2 CAS與原子類
2.1 CAS
CAS即 Compare and Swap,體現(xiàn)樂(lè)觀鎖的思想。

獲取共享變量時(shí),為了保證變量的可見性,需要使用volatile修飾。結(jié)合CAS和volatile可以實(shí)現(xiàn)無(wú)鎖并發(fā),適用于競(jìng)爭(zhēng)不激烈、多核CPU的場(chǎng)景下。
- 因?yàn)闆]有使用synchronized,所以線程不會(huì)陷入阻塞,這是效率提升的因素之一
- 但如果競(jìng)爭(zhēng)激烈,重試必然頻繁發(fā)生,反而效率會(huì)受影響
CAS底層依賴于一個(gè)Unsafe類來(lái)直接調(diào)用操作系統(tǒng)底層的CAS指令。
2.2 樂(lè)觀鎖與悲觀鎖
java中的樂(lè)觀鎖其實(shí)就是CAS,悲觀鎖就是synchronized。
- CAS集于樂(lè)觀鎖的思想:最樂(lè)觀的估計(jì),不怕別的線程來(lái)修改共享變量,就算改了也沒有關(guān)系,我吃虧點(diǎn)在重試。
- synchronized是基于悲觀鎖的思想:最悲觀的估計(jì),得放著其他線程來(lái)修改共享變量,我上了鎖你們都別想改,我改完了釋放鎖你們才能改。
2.3 原子操作類
juc(java.until.concurrent)中提供了原子操作類,可以提供線程安全的操作,例如:AtomicInteger、AtomicBoolean等,它們底層就是采用CAS技術(shù)+volatile來(lái)實(shí)現(xiàn)的。
但是CAS會(huì)存在ABA問(wèn)題。
3 synchronized
java HotSpot虛擬機(jī)中,每個(gè)對(duì)下你個(gè)都有對(duì)象頭(包括class指針和Mark Word)。Mark Word平時(shí)存儲(chǔ)該對(duì)象的 哈希碼、分代年齡,當(dāng)加鎖時(shí),這些信息就根據(jù)情況被替換為 標(biāo)記位、線程鎖記錄指針、重量級(jí)鎖指針、線程ID等內(nèi)容。
3.1 輕量級(jí)鎖
多個(gè)線程交替執(zhí)行,不存在競(jìng)爭(zhēng),那么可以使用輕量級(jí)鎖進(jìn)行優(yōu)化。
3.2 鎖膨脹
如果在嘗試加輕量級(jí)鎖的過(guò)程中,CAS操作無(wú)法成功,這時(shí)一種情況就是有其他線程為此對(duì)象加上了輕量級(jí)鎖(有競(jìng)爭(zhēng)),這時(shí)需要進(jìn)行鎖膨脹,將輕量級(jí)鎖變?yōu)橹亓考?jí)鎖。
3.3 重量鎖
重量級(jí)鎖競(jìng)爭(zhēng)的時(shí)候,還可以使用自旋來(lái)進(jìn)行優(yōu)化,如果當(dāng)前線程自旋成功(即這時(shí)候持鎖線程已經(jīng)退出了同步塊,釋放了鎖),這時(shí)當(dāng)前線程就可以避免阻塞。
在Java6之后自旋鎖是自適應(yīng)的,比如對(duì)象剛剛的一次自旋操作成功過(guò),那么認(rèn)為這次自旋成功的可能性會(huì)高,就多自旋幾次;反之,就少自旋甚至不自旋,總之,比較智能。
- 自旋會(huì)占用CPU時(shí)間,單核CPU自旋就是浪費(fèi)時(shí)間,多核CPU自旋才能發(fā)揮優(yōu)勢(shì)。
- 好比等紅燈時(shí)汽車是不是熄火,不熄火相當(dāng)于自旋(等待時(shí)間短了劃算),熄火了相當(dāng)于阻塞(等待時(shí)間長(zhǎng)了劃算)。
- Java7后不能控制是否開啟自旋功能。
3.4 偏向鎖
輕量級(jí)鎖在沒有競(jìng)爭(zhēng)時(shí),每次重入仍然需要執(zhí)行CAS操作。Java6中引入了偏向鎖來(lái)做進(jìn)一步優(yōu)化:只有第一次使用CAS將線程ID設(shè)置到對(duì)象的Mark Word頭,之后發(fā)現(xiàn)這個(gè)線程ID是自己的就表示沒有競(jìng)爭(zhēng),不用重新CAS。
- 撤銷偏向需要將持鎖線程升級(jí)為輕量級(jí)鎖,這個(gè)過(guò)程中所有線程需要暫停(STW)
- 訪問(wèn)對(duì)象的hashCode也會(huì)撤銷偏向鎖
- 如果對(duì)象雖然被多個(gè)線程訪問(wèn),但沒有競(jìng)爭(zhēng),這時(shí)偏向了線程T1的對(duì)象仍有機(jī)會(huì)重新偏向T2,重偏向會(huì)重置對(duì)象的Thread ID
- 撤銷偏向和重偏向都是批量進(jìn)行的,以類為單位
- 如果撤銷偏向達(dá)到某個(gè)閾值,整個(gè)類的所有對(duì)象都會(huì)變?yōu)椴豢善虻?/li>
- 可以主動(dòng)使用 -XX:-UseBiasedLocking 禁用偏向鎖
3.5 其他優(yōu)化
3.5.1 減少上鎖時(shí)間
同步代碼塊中盡量短
3.5.2 減少鎖的粒度
將一個(gè)鎖拆分為多個(gè)鎖提高并發(fā)度,如:
- ConcurrentHashMap
- LongAdder 分為base和cells兩部分。沒有并發(fā)爭(zhēng)用的時(shí)候或者是cells數(shù)組正在初始化的時(shí)候,會(huì)使用CAS來(lái)累加值到base,有并發(fā)爭(zhēng)用,會(huì)初始化cells數(shù)組,數(shù)組有多好個(gè)cell,就允許有多少線程并行修改,最后將數(shù)組中每個(gè)cell累加,在加上base就是最終的值
- LinkedBlockingQueue 入隊(duì)和出隊(duì)使用不同的鎖,想對(duì)于LinkedBlockingArray只有一個(gè)鎖效率更高
3.5.3 鎖粗化
多次循環(huán)進(jìn)入同步塊不如同步塊內(nèi)多次循環(huán)
另外JVM可能會(huì)做如下優(yōu)化,把多次append的加鎖操作粗化為一次(因?yàn)槎际菍?duì)同一個(gè)對(duì)象加鎖,沒必要重入多次)
new StringBuffer().append("a").append("b").append("c");
3.5.4 鎖消除
JVM會(huì)進(jìn)行代碼的逃逸分析,例如某個(gè)加鎖對(duì)象是方法內(nèi)的局部變量,不會(huì)被其他線程所訪問(wèn)到,這時(shí)候就會(huì)被即時(shí)編譯器忽略掉所有的同步操作。
3.5.5 讀寫分離
CopyOnWriteArrayList
ConvOnWriteSet