自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

線(xiàn)程池和ReentrantLock背后的最強(qiáng)支柱:volatile

開(kāi)發(fā) 前端
本篇文章從volatile的特性展開(kāi),介紹到了Java的JMM(Java內(nèi)存模型)模型,有些同學(xué)這個(gè)時(shí)候心里就要開(kāi)始迷糊了,我聽(tīng)過(guò)Java對(duì)象模型、JVM內(nèi)存模型,那它們又是干什么用的呢?我知道你很急,但是你不用急,下篇文章接著解答的疑惑。?

一、前言

在前幾篇文章中,我們?cè)诜治鼍€(xiàn)程池和ReentrantLock的時(shí)候,其內(nèi)部實(shí)現(xiàn)大量用到了volatile關(guān)鍵字來(lái)修飾變量,前面我們也簡(jiǎn)單分析過(guò)使用volatile是為了用它的內(nèi)存可見(jiàn)性。除了內(nèi)存可見(jiàn)性,它還有哪些能力呢?這篇文章來(lái)詳細(xì)告訴你。

二、大象裝進(jìn)冰箱的case

給你一臺(tái)足夠大的冰箱,把大象塞進(jìn)去至少需要三步,第一步打開(kāi)冰箱門(mén),第二步將大象搬進(jìn)去,第三步將冰箱門(mén)關(guān)上。我們來(lái)假設(shè)一個(gè)場(chǎng)景:冰箱只有一臺(tái)且同一時(shí)刻只能放入一只大象,但在某一時(shí)刻有5只大象都要進(jìn)入冰箱降暑,那么在大象裝進(jìn)冰箱這件事情的整個(gè)過(guò)程中,中間任一步驟失敗就會(huì)直接導(dǎo)致整件事情的失敗。如果不想存在中間過(guò)程中出現(xiàn)失敗的可能,只有一個(gè)辦法這件事件的三個(gè)步驟合三為一,使其成為一個(gè)整體,從外部看就像只有一個(gè)“將大象塞進(jìn)冰箱”動(dòng)作。我們?cè)诙嗑€(xiàn)程環(huán)境下對(duì)一個(gè)變量進(jìn)行操作時(shí),會(huì)經(jīng)常遇到這種問(wèn)題,下面我們來(lái)看看如何完美解決。

二、Java內(nèi)存模型

想要完美解決多線(xiàn)程下對(duì)同一變量進(jìn)行安全操作,我們得先要了解清楚Java內(nèi)存模型,內(nèi)存模型如下圖所示

圖片圖片

  1. Java內(nèi)存模型規(guī)定了所有的變量都必須存儲(chǔ)在主內(nèi)存中,而每條工作線(xiàn)程有自己的工作內(nèi)存,工作內(nèi)存中存儲(chǔ)的的是該線(xiàn)程執(zhí)行過(guò)程中臨時(shí)用到的變量信息,這些信息都是從主內(nèi)存中拷貝的副本,另外線(xiàn)程對(duì)變量的所有操作行為都必須在工作內(nèi)存完成,而不能直接操作主內(nèi)存中的變量信息。
  2. 不同線(xiàn)程之間也無(wú)法直接訪(fǎng)問(wèn)對(duì)方工作內(nèi)存中的變量,線(xiàn)程間變量的傳遞均需通過(guò)自己的工作內(nèi)存和主內(nèi)存之間進(jìn)行數(shù)據(jù)交互,然后再傳遞到別的線(xiàn)程工作內(nèi)存中完成信息的交互。

小結(jié):JMM(Java Memory Model)是一種規(guī)范,目的是解決由于多線(xiàn)程通過(guò)共享內(nèi)存進(jìn)行通信時(shí),存儲(chǔ)在工作內(nèi)存的數(shù)據(jù)不一致、編譯器會(huì)對(duì)代碼指令重排序、處理器會(huì)對(duì)代碼亂序執(zhí)行等帶來(lái)的問(wèn)題。

三、volatile三大屬性

2.1 原子性

2.1.1 volatile為什么不能保證原子性

/**
 * @author 程序反思錄 <程序反思錄@xxx.com>
 * Created on 2024-09-29
 */
public class MultiThreadCount {
    private volatile int salesCount = 0;

    public void addSalesCount() {
        salesCount++;
    }

    public static void main(String[] args) {
        MultiThreadCount multiThreadCount = new MultiThreadCount();
        new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                multiThreadCount.addSalesCount();
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                multiThreadCount.addSalesCount();
            }
        }).start();
        new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                multiThreadCount.addSalesCount();
            }
        }).start();

        System.out.println(multiThreadCount.salesCount);
    }
}

運(yùn)行上面這段代碼,在不同的機(jī)器上得到的結(jié)果大概率都不一樣且結(jié)果值都不是3000。

現(xiàn)在我們?cè)倩剡^(guò)頭來(lái)分析上面的那段示例代碼,剛開(kāi)始3個(gè)線(xiàn)程分別從主內(nèi)存copy salesCount=0到各自的工作內(nèi)存中去,然后分別執(zhí)行自增操作,完后后將各自的值刷回到主內(nèi)存,一次salesCount自增操作會(huì)涉及三步操作(就像將大象放入冰箱的case一樣),多個(gè)線(xiàn)程同時(shí)多次執(zhí)行這三步操作勢(shì)必會(huì)造成主內(nèi)存中值被覆蓋情況,這也就解釋了volatile沒(méi)能保證原子性的原因。

2.1.2 如何實(shí)現(xiàn)原子性

解決上面的問(wèn)題很容易,只需要將salesCount的修飾由volatile改成就可以了,代碼如下

private AtomicInteger salesCount = new AtomicInteger(0);

public void addSalesCount() {
    salesCount.incrementAndGet();
}

有同學(xué)就會(huì)好奇了,為什么AtomicInteger就可以解決數(shù)據(jù)被刷回到主內(nèi)存后數(shù)據(jù)被覆蓋的問(wèn)題呢?點(diǎn)開(kāi)AtomicInteger的源碼會(huì)有有兩個(gè)關(guān)鍵的動(dòng)作:

  1. AtomicInteger內(nèi)部維護(hù)的value屬性是用volatile修飾的,利用其內(nèi)存可見(jiàn)性的特性使得值被修改后,別的線(xiàn)程能夠及時(shí)感知到(后面分析內(nèi)存可見(jiàn)性的時(shí)候再展開(kāi))
  2. 使用了CAS特性加死循環(huán)來(lái)保證值不會(huì)被覆蓋,并將當(dāng)前最新值累加上去刷回到主內(nèi)存,我們稍微展開(kāi)分析一下具體實(shí)現(xiàn)
// 調(diào)用該方法對(duì)計(jì)數(shù)器進(jìn)行+1操作
public final int incrementAndGet() {
 // 通過(guò)unsafe類(lèi)實(shí)現(xiàn)原子加+1操作
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

// 1. 首先通過(guò)CAS嘗試將+1后的數(shù)據(jù)寫(xiě)入到工作線(xiàn)程,
//    然后回寫(xiě)到主內(nèi)存(這里會(huì)通過(guò)lock指令強(qiáng)制將修改后的值回寫(xiě)到主內(nèi)存,
//    下面分析可見(jiàn)性的時(shí)候在展開(kāi))。
// 2. 如果CAS操作失敗了,通過(guò)while死循環(huán)不斷自旋,直到最新值被成功回寫(xiě)到主內(nèi)存,
//    說(shuō)點(diǎn)題外話(huà),相信看過(guò)線(xiàn)程池和ReentrantLock文章的同學(xué)會(huì)有感覺(jué),
//    一般CAS出現(xiàn)的地方,會(huì)伴隨著死循環(huán)的身影出現(xiàn)。
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

2.2 內(nèi)存可見(jiàn)性

2.2.1 什么是內(nèi)存可見(jiàn)性

內(nèi)存可見(jiàn)性(Memory Visibility)是指在一個(gè)線(xiàn)程中修改了某個(gè)變量的值之后,這些修改能夠被其他線(xiàn)程立即看到。在多線(xiàn)程環(huán)境中,由于每個(gè)線(xiàn)程可能有自己的工作內(nèi)存(緩存),而不是直接操作主內(nèi)存,因此會(huì)出現(xiàn)內(nèi)存可見(jiàn)性問(wèn)題。

2.2.2 volatile是如何解決內(nèi)存可見(jiàn)性的問(wèn)題

當(dāng)對(duì)volatile修飾的變量進(jìn)行修改時(shí),JVM會(huì)向處理器發(fā)送一條lock前綴的指令,將當(dāng)前處理器中緩存的最新值強(qiáng)制寫(xiě)回到主存中,所有處理器都需要遵守緩存一致性協(xié)議,當(dāng)其他處理器發(fā)現(xiàn)自己緩存的數(shù)據(jù)已經(jīng)被修改,則會(huì)從主存中拉取最新的值緩存到自己的緩存內(nèi),從而實(shí)現(xiàn)了可見(jiàn)性的特性。 緩存一致性協(xié)議:每個(gè)處理器通過(guò)嗅探在總線(xiàn)上傳播的數(shù)據(jù)來(lái)檢查自己緩存的值是否已過(guò)期,當(dāng)處理器發(fā)現(xiàn)自己緩存行的內(nèi)存地址被修改,就會(huì)將當(dāng)前處理器的緩存設(shè)置成無(wú)效狀態(tài),當(dāng)處理器要對(duì)這個(gè)數(shù)據(jù)進(jìn)行修改操作時(shí),會(huì)強(qiáng)制從主存讀取最新數(shù)據(jù)寫(xiě)入到處理器緩存中。

2.2.3 解決內(nèi)存可見(jiàn)性問(wèn)題的替代方案

i) 通過(guò)鎖來(lái)解決同一時(shí)刻只有一個(gè)線(xiàn)程可以修改值

  • 使用synchroized關(guān)鍵字,保證多個(gè)線(xiàn)程操作時(shí),只有搶到鎖的線(xiàn)程才可以執(zhí)行修改操作
  • 使用Atomic類(lèi),通過(guò)CAS+死循環(huán)的方式

ii) 使用final關(guān)鍵字修飾,使得變量不能被修改,從而避開(kāi)了內(nèi)存可見(jiàn)性問(wèn)題的發(fā)生

2.3 指令重排

2.3.1 什么是指令重排

指令重排是指編譯器、運(yùn)行時(shí)系統(tǒng)或處理器為了優(yōu)化性能,對(duì)程序中的指令順序進(jìn)行調(diào)整的過(guò)程。

2.3.2 指令重排有什么好處

i) 編譯器優(yōu)化:編譯器可能會(huì)對(duì)代碼進(jìn)行重排序,以減少寄存器的使用、提高指令流水線(xiàn)的效率等。

ii) 運(yùn)行時(shí)系統(tǒng)優(yōu)化:運(yùn)行時(shí)系統(tǒng)可能會(huì)對(duì)字節(jié)碼進(jìn)行優(yōu)化,以提高執(zhí)行效率。

iii) 處理器優(yōu)化:現(xiàn)代處理器具有復(fù)雜的流水線(xiàn)和多級(jí)緩存,可能會(huì)對(duì)指令進(jìn)行重排序以提高性能。

2.3.3 為什么volatile禁止指令重排

大多數(shù)情況下指令重排這種優(yōu)化操作是透明的,但在多線(xiàn)程環(huán)境中,指令重排可能會(huì)導(dǎo)致一些問(wèn)題 i) 內(nèi)存可見(jiàn)性問(wèn)題:由于指令執(zhí)行順序被重排,使得修改操作被延遲觸發(fā),最終導(dǎo)致一個(gè)線(xiàn)程對(duì)變量的修改可能不會(huì)理解對(duì)其他線(xiàn)程可見(jiàn)。ii) 競(jìng)態(tài)條件:指令重排可能導(dǎo)致兩個(gè)線(xiàn)程之間的操作順序不符合預(yù)期,從而引發(fā)競(jìng)態(tài)條件。

2.3.4 禁止指令重排是如何實(shí)現(xiàn)的

禁止指令重排序是通過(guò)內(nèi)存屏障來(lái)實(shí)現(xiàn)的。內(nèi)存屏障是一種特殊的指令,它可以確保某些操作在屏障前后按照特定的順序執(zhí)行,從而防止編譯器、運(yùn)行時(shí)系統(tǒng)和處理器對(duì)這些操作進(jìn)行重排序。內(nèi)存屏障分為兩種:i) 寫(xiě)屏障:在寫(xiě)操作之后插入一個(gè)寫(xiě)屏障,確保所有之前的寫(xiě)操作都已完成并回寫(xiě)到主內(nèi)存中。ii) 讀屏障:在讀操作之前插入一個(gè)讀屏障,確保所有后續(xù)的讀操作都從主內(nèi)存中讀取最新的值。

四、后續(xù)

本篇文章從volatile的特性展開(kāi),介紹到了Java的JMM(Java內(nèi)存模型)模型,有些同學(xué)這個(gè)時(shí)候心里就要開(kāi)始迷糊了,我聽(tīng)過(guò)Java對(duì)象模型、JVM內(nèi)存模型,那它們又是干什么用的呢?我知道你很急,但是你不用急,下篇文章接著解答的疑惑。

責(zé)任編輯:武曉燕 來(lái)源: 程序反思錄
相關(guān)推薦

2021-09-11 15:26:23

Java多線(xiàn)程線(xiàn)程池

2013-08-14 16:30:36

NEC高可用集群軟件

2020-06-27 08:41:31

機(jī)器學(xué)習(xí)數(shù)學(xué)算法

2020-06-30 09:06:05

機(jī)器學(xué)習(xí)數(shù)學(xué)深度學(xué)習(xí)

2012-05-15 02:18:31

Java線(xiàn)程池

2024-11-27 08:15:50

2021-06-06 23:40:53

線(xiàn)程池使用場(chǎng)景

2011-07-25 15:17:10

iPhone 操作隊(duì)列 Java

2020-07-17 20:15:03

架構(gòu)JMMvolatile

2021-03-08 08:55:22

開(kāi)發(fā)

2023-12-14 15:05:08

volatile代碼C++

2023-05-19 08:01:24

Key消費(fèi)場(chǎng)景

2019-10-30 21:27:51

Java中央處理器電腦

2011-08-19 17:36:42

iPhone操作隊(duì)列Java

2020-07-28 08:00:48

池化線(xiàn)程線(xiàn)程池

2024-07-15 08:20:24

2021-02-05 11:35:03

原子類(lèi)數(shù)值變量

2023-11-29 16:38:12

線(xiàn)程池阻塞隊(duì)列開(kāi)發(fā)

2021-06-17 06:57:10

SpringBoot線(xiàn)程池設(shè)置

2015-08-20 09:17:36

Java線(xiàn)程池
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)