java 內(nèi)存模型 JMM

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

image.png

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í)候值為臟值。

image.png

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后并沒有讓線程停止,這是由于:

  1. 初始狀態(tài),t線程剛開始從主內(nèi)存讀取了run的值加載到工作內(nèi)存。
image.png
  1. 因?yàn)閠線程要頻繁從主存中讀取run的值,JIT編譯器會(huì)將run的值緩存至自己工作內(nèi)存中的高速緩存中,減少對(duì)主存中run的訪問(wèn),提高效率。
image.png
  1. 1秒之后,main線程修改了run值,并同步至主存,而t是從自己的工作內(nèi)存中的告訴緩存中讀取run,結(jié)果永遠(yuǎn)是舊值。
image.png
  1. 解決方式:
  • 可以通過(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è)觀鎖的思想。

image.png

獲取共享變量時(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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容