溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

Volatile的作用是什么

發(fā)布時(shí)間:2021-10-26 17:14:45 來(lái)源:億速云 閱讀:243 作者:iii 欄目:編程語(yǔ)言

這篇文章主要介紹“Volatile的作用是什么”,在日常操作中,相信很多人在Volatile的作用是什么問(wèn)題上存在疑惑,小編查閱了各式資料,整理出簡(jiǎn)單好用的操作方法,希望對(duì)大家解答”Volatile的作用是什么”的疑惑有所幫助!接下來(lái),請(qǐng)跟著小編一起來(lái)學(xué)習(xí)吧!

1、相關(guān)了解

1.1.現(xiàn)代計(jì)算機(jī)的內(nèi)存模型

其實(shí)早期計(jì)算機(jī)中cpu和內(nèi)存的速度是差不多的,但在現(xiàn)代計(jì)算機(jī)中,cpu的指令速度遠(yuǎn)超內(nèi)存的存取速度,由于計(jì)算機(jī)的儲(chǔ)存設(shè)備與處理器的運(yùn)算速度有幾個(gè)數(shù)量級(jí)的差距,所以現(xiàn)代計(jì)算機(jī)系統(tǒng)都不得不加入一層讀寫(xiě)速度盡可能接近處理器運(yùn)算速度的 高速緩存(Cach) 來(lái)作為內(nèi)存與處理器之間的緩沖。

將運(yùn)算需要使用到的數(shù)據(jù)復(fù)制到緩存中,讓運(yùn)算能快速進(jìn)行,當(dāng)運(yùn)算結(jié)束后再?gòu)木彺嫱交貎?nèi)存之中,這樣處理器就無(wú)需等待緩慢的內(nèi)存讀寫(xiě)了。

基于高速緩存的存儲(chǔ)交互很好地解決了處理器與內(nèi)存的速度矛盾,但是也為計(jì)算機(jī)系統(tǒng)帶來(lái)更高的復(fù)雜度,因?yàn)樗胍粋€(gè)新的問(wèn)題:緩存一致性(CacheCoherence)

在多處理器系統(tǒng)中,每個(gè)處理器都有自己的高速緩存,而它們又共享同一主內(nèi)存(MainMemory)。

Volatile的作用是什么

1.2.JMM(JavaMemoryModel)

JMM:Java 內(nèi)存模型,是java虛擬機(jī)規(guī)范中所定義的一種內(nèi)存模型,Java 內(nèi)存模型是標(biāo)準(zhǔn)化的,屏蔽掉了底層不同計(jì)算機(jī)的區(qū)別。描述了Java程序中各種變量(線程共享變量)的訪問(wèn)規(guī)則,以及在JVM中將變量,儲(chǔ)存到內(nèi)存和從內(nèi)存中讀取變量這樣的底層細(xì)節(jié)。

1.2.1JMM有以下規(guī)定

所有的共享變量都儲(chǔ)存于主內(nèi)存,這里所說(shuō)的變量指的是實(shí)例變量和類(lèi)變量,不包含局部變量,因?yàn)榫植孔兞渴蔷€程私有的,因此不存在競(jìng)爭(zhēng)問(wèn)題。

每一個(gè)線程還存在自己的工作內(nèi)存,線程的工作內(nèi)容,保留了被線程使用的變量的工作副本。

線程對(duì)變量的所有操作(讀、取)都必須在工作內(nèi)存中完成,而不能直接讀寫(xiě)主內(nèi)存中的變量。

不同線程之間也不能直接訪問(wèn)對(duì)方工作內(nèi)存中的變量,線程間變量的值的傳遞需要通過(guò)主內(nèi)存中轉(zhuǎn)來(lái)完成。

本地內(nèi)存和主內(nèi)存的關(guān)系:

Volatile的作用是什么

正是因?yàn)檫@樣的機(jī)制,才導(dǎo)致了可見(jiàn)性問(wèn)題的存在。

2、可見(jiàn)性的解決方案

加鎖

/**
 * @Author 庭前云落
 * @Date 2020/10/1 10:43
 * @Description
 */
public class Test {
    public static void main(String[] args) {
        Tqyl a = new Tqyl();
        a.start();
        for (; ; ) {
            synchronized (a) {
                if (a.isFlag()) {
                    System.out.println("庭前云落");
                }
            }
        }
    }
}

2.1.為什么加鎖可以解決可見(jiàn)性問(wèn)題呢?

因?yàn)槟骋粋€(gè)線程進(jìn)入synchronized代碼塊前后,線程會(huì)獲得鎖,清空工作內(nèi)存,從主內(nèi)存拷貝共享變量最新的值到工作內(nèi)存成為副本,執(zhí)行代碼,將修改后的副本的值刷新回主內(nèi)存中,線程釋放鎖。

而獲取不到鎖的線程會(huì)阻塞等待,所以變量的值肯定一直都是最新的。

2.2.Volatile修飾共享變量

/**
 * @Author 庭前云落
 * @Date 2020/10/1 10:44
 * @Description
 */
public class Tqyl extends Thread {
    private volatile boolean flag = false;

    public boolean isFlag() {
        return flag;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag=true;
        System.out.println("flag="+flag);
    }
}

2.3.Volatile做了什么?

每個(gè)線程操作數(shù)據(jù)的時(shí)候會(huì)把數(shù)據(jù)從主內(nèi)存讀取到自己的工作內(nèi)存,如果它操作了數(shù)據(jù)并且寫(xiě)回了,其它已經(jīng)讀取的線程的變量副本就會(huì)失效了,需要讀數(shù)據(jù)進(jìn)操作又要再次去主內(nèi)存中讀取了。

volatile保證不同線程對(duì)共享變量操作的可見(jiàn)性,也就是說(shuō)一個(gè)線程修改了volatile修飾的變量,當(dāng)修改寫(xiě)回主內(nèi)存時(shí),另外一個(gè)線程立即看到最新的值。


之前我們說(shuō)過(guò)當(dāng)多個(gè)處理器的運(yùn)算任務(wù)都涉及同一塊主內(nèi)存區(qū)域時(shí),將可能導(dǎo)致各自的緩存數(shù)據(jù)不一致,舉例說(shuō)明變量在多個(gè)CPU之間的共享。

如果真的發(fā)生這種情況,那同步回到主內(nèi)存時(shí)以誰(shuí)的緩存數(shù)據(jù)為準(zhǔn)呢?

為了解決一致性的問(wèn)題,需要各個(gè)處理器訪問(wèn)緩存時(shí)都遵循一些協(xié)議,在讀寫(xiě)時(shí)要根據(jù)協(xié)議來(lái)進(jìn)行操作,這類(lèi)協(xié)議有 MSI、MESI(IllinoisProtocol)、MOSI、Synapse、Firefly及DragonProtocol等。

3、MESI(緩存一致性協(xié)議)

當(dāng)CPU寫(xiě)數(shù)據(jù)時(shí),如果發(fā)現(xiàn)操作的變量是共享變量,即在其它CPU中野存在該變量的副本,會(huì)發(fā)出信息通知其它CPU將該變量的緩存行置為無(wú)效狀態(tài),因此當(dāng)其他CPU需要讀取這個(gè)變量時(shí),發(fā)現(xiàn)自己緩存中緩存該變量的緩存行是無(wú)效的,那么它就會(huì)從內(nèi)存重新讀取。

3.1.至于怎么發(fā)現(xiàn)數(shù)據(jù)是否失效呢?

嗅探(竊聽(tīng)網(wǎng)絡(luò)上流經(jīng)的數(shù)據(jù)包)

每個(gè)處理器通過(guò)嗅探在總線上傳播的數(shù)據(jù)來(lái)檢查自己緩存的值是不是過(guò)期了,當(dāng)處理器發(fā)現(xiàn)自己緩存行對(duì)應(yīng)的內(nèi)存地址被修改,就會(huì)將當(dāng)前處理器的緩存行設(shè)置成無(wú)效狀態(tài),當(dāng)處理器對(duì)這個(gè)數(shù)據(jù)進(jìn)行修改操作的時(shí)候,會(huì)重新從系統(tǒng)內(nèi)存中把數(shù)據(jù)讀到處理器緩存里。

4、總線風(fēng)暴

由于Volatile的MESI緩存一致性協(xié)議,需要不斷的從主內(nèi)存嗅探和cas不斷循環(huán),無(wú)效交互會(huì)導(dǎo)致總線帶寬達(dá)到峰值。

所以不要大量使用Volatile,至于什么時(shí)候去使用Volatile什么時(shí)候使用鎖,根據(jù)場(chǎng)景區(qū)分。

CAS(Compare-and-Swap):即比較并替換,是一種實(shí)現(xiàn)并發(fā)算法時(shí)常用到的技術(shù),Java并發(fā)包中的很多類(lèi)都使用了CAS技術(shù)。

5、禁止指令重排序

5.1.什么是重排序?

為了提高性能,編譯器和處理器常常會(huì)對(duì)既定的代碼執(zhí)行順序進(jìn)行指令重排序。

5.2.重排序的類(lèi)型有哪些呢?源碼到最終執(zhí)行會(huì)經(jīng)過(guò)哪些重排序呢?

Volatile的作用是什么

一個(gè)好的內(nèi)存模型實(shí)際上會(huì)放松對(duì)處理器和編譯器規(guī)則的束縛,也就是說(shuō)軟件技術(shù)和硬件技術(shù)都為同一個(gè)目標(biāo),而進(jìn)行奮斗:在不改變程序執(zhí)行結(jié)果的前提下,盡可能提高執(zhí)行效率。

JMM對(duì)底層盡量減少約束,使其能夠發(fā)揮自身優(yōu)勢(shì)。

因此,在執(zhí)行程序時(shí),為了提高性能,編譯器和處理器常常會(huì)對(duì)指令進(jìn)行重排序。

一般重排序可以分為如下三種:

  • 編譯器優(yōu)化的重排序。編譯器在不改變單線程程序語(yǔ)義的前提下,可以重新安排語(yǔ)句的執(zhí)行順序;

  • 指令級(jí)并行的重排序。現(xiàn)代化處理器采用了指令級(jí)并行技術(shù)來(lái)將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴(lài)性,處理器可以改變語(yǔ)句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序;

  • 內(nèi)存系統(tǒng)的重排序。由于處理器使用緩存和讀/寫(xiě)緩沖區(qū),這使得加載和儲(chǔ)存操作看上去可能是在亂序執(zhí)行的。

這里還得提一個(gè)概念,as-if-serial。

5.3.as-if-serial

不管怎么重排序,單線程下的執(zhí)行結(jié)果不能被改變。

編譯器、runtime和處理器都必須遵守as-if-serial語(yǔ)義。

那Volatile是怎么保證不會(huì)被執(zhí)行重排序的呢?

5.4.內(nèi)存屏障

java編譯器會(huì)在生成指令系列時(shí)在適當(dāng)?shù)奈恢脮?huì)插入 內(nèi)存屏障 指令來(lái)禁止特定類(lèi)型的處理器重排序。

為了實(shí)現(xiàn)volatile的內(nèi)存語(yǔ)義,JMM會(huì)限制特定類(lèi)型的編譯器和處理器重排序,JMM會(huì)針對(duì)編譯器制定volatile重排序規(guī)則表:

Volatile的作用是什么

需要注意的是:volatile寫(xiě)是在前?和后?分別插?內(nèi)存屏障,?volatile讀操作是在后面插?兩個(gè)內(nèi)存屏障

寫(xiě)

Volatile的作用是什么

Volatile的作用是什么

上面的我提過(guò)重排序原則,為了提高處理速度,JVM會(huì)對(duì)代碼進(jìn)行編譯優(yōu)化,也就是指令重排序優(yōu)化,并發(fā)編程下指令重排序會(huì)帶來(lái)一些安全隱患:如指令重排序?qū)е碌亩鄠€(gè)線程操作之間的不可見(jiàn)性。

如果讓程序員再去了解這些底層的實(shí)現(xiàn)以及具體規(guī)則,那么程序員的負(fù)擔(dān)就太重了,嚴(yán)重影響了并發(fā)編 程的效率。

從JDK5開(kāi)始,提出了 happens-before 的概念,通過(guò)這個(gè)概念來(lái)闡述操作之間的內(nèi)存可?性。

5.5.happens-before

如果一個(gè)操作執(zhí)行的結(jié)果需要對(duì)另一個(gè)操作可見(jiàn),那么這兩個(gè)操作之間必須存在 happens-before關(guān)系。

volatile域規(guī)則:對(duì)一個(gè) volatile 域的寫(xiě)操作,happens-before 于任意線程后續(xù)對(duì)這個(gè)volatile域的讀。

如果現(xiàn)在我的flag變成了false,那么后面的那個(gè)操作,一定要知道我變了。

我們要知道Volatile是沒(méi)辦法保證原子性的,一定要保證原子性,可以使用其他方法。

無(wú)法保證原子性

就是一次操作,要么完全成功,要么完全失敗。

假設(shè)現(xiàn)在有 N 個(gè)線程對(duì)同一個(gè)變量進(jìn)行累加也是沒(méi)辦法保證結(jié)果是對(duì)的,因?yàn)樽x寫(xiě)這個(gè)過(guò)程并不是原子性的。

要解決也簡(jiǎn)單,要么用原子類(lèi),比如 AtomicInteger,要么加鎖(記得關(guān)注Atomic的底層)

應(yīng)用

/**
 * @Author 庭前云落
 * @Date 2020/10/1 11:53
 * @Description
 */
public class Singleton {
    //可見(jiàn)性和指令重排序都保證
    private volatile static Singleton instance = null;
    
    //私有構(gòu)造
    public Singleton() {
    }
    
    public static Singleton getInstance(){
        //第一重檢查鎖定
        if(instance==null){
            //同步鎖定代碼塊
            synchronized (Singleton.class){
                //第二重檢查鎖定
                if(instance==null){
                    //注意:非原子操作
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

為什么要雙重檢查?如果不用Volatile會(huì)怎么樣?

禁止指令重排序的好處。

對(duì)象實(shí)際上創(chuàng)建對(duì)象要經(jīng)過(guò)如下幾個(gè)步驟:

  • 分配內(nèi)存空間

  • 調(diào)用構(gòu)造器,初始化實(shí)例

  • 返回地址給引用

是可能發(fā)生指令重排序的,那有可能構(gòu)造函數(shù)在對(duì)象初始化完成前就賦值完成了,在內(nèi)存里面開(kāi)辟一片儲(chǔ)存區(qū)域后直接返回內(nèi)存的引用,這個(gè)時(shí)候還沒(méi)真正的初始化完對(duì)象。

但是別的線程去判斷 instance!=null,直接拿去用了,其實(shí)這個(gè)對(duì)象是個(gè)半成品,那就有空指針異常了。

可見(jiàn)性怎么保證的?

因?yàn)榭梢?jiàn)性,線程A在自己的內(nèi)存初始化了對(duì)象,還沒(méi)來(lái)得及寫(xiě)回主內(nèi)存,B線程也這么做了,那就創(chuàng)建了多個(gè)對(duì)象,不是真正意義上單例了。

6、Volatile與Synchronized的區(qū)別

volatile只能修飾實(shí)例變量和類(lèi)變量,而synchronized可以修飾方法,以及代碼塊。

volatile保證數(shù)據(jù)的可見(jiàn)性,但是不保證原子性(多線程進(jìn)行寫(xiě)操作,不保證線程安全);而synchronized是一種排他(互斥)的機(jī)制。volatile用于禁止指令重排序:可以解決單例雙重檢查對(duì)象初始化代碼執(zhí)行亂序問(wèn)題。

volatile 可以看做輕量版的synchronized,volatile不保證原子性,但是如果是對(duì)一個(gè)共享變量進(jìn)行多個(gè)線程的賦值,而沒(méi)有其它的操作,那么就可以用 volatile 來(lái)代替 synchronized,因?yàn)橘x值本身就是有原子性的,而 volatile 又保證了可見(jiàn)性,所以就可以保證線程安全了。

7、總結(jié)

  1. volatile修飾符適用于以下的場(chǎng)景:

    某個(gè)屬性被多個(gè)線程共享,其中有一個(gè)線程修改了此屬性,其它線程可以立即得到修改后的值,比如 booleanflag;或者作為觸發(fā)器,實(shí)現(xiàn)輕量級(jí)同步。

  2. volatile屬性的讀寫(xiě)操作都是無(wú)鎖的,它不能替代synchronized,因?yàn)樗鼪](méi)有提供原子性和互斥性。因?yàn)闊o(wú)鎖,不需要花費(fèi)時(shí)間在獲取鎖和釋放鎖上,所以說(shuō)它是低成本的。

  3. volatile只能作用于屬性,我們用volatile修飾屬性,這樣compilers就不會(huì)對(duì)這個(gè)屬性做指令重排序。

  4. volatile提供了可見(jiàn)性,任何一個(gè)線程對(duì)其的修改將立馬對(duì)其它線程可見(jiàn),volatile屬性不會(huì)被線程緩存,始終從主存中讀取。

  5. volatile提供了happens-before保證,對(duì)volatile變量v的寫(xiě)入happens-before所有其他線程后續(xù)對(duì)v的讀寫(xiě)操作。

  6. volatile可以使得long和double的賦值是原子的。

  7. volatile可以在單例雙重檢查中實(shí)現(xiàn)可見(jiàn)性和禁止指令重排序,從而保證安全性。

到此,關(guān)于“Volatile的作用是什么”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請(qǐng)繼續(xù)關(guān)注億速云網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)?lái)更多實(shí)用的文章!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI