細(xì)說(shuō)Java多線程之內(nèi)存可見(jiàn)性

可見(jiàn)性介紹

可見(jiàn)性:一個(gè)線程對(duì)共享變量的值的修改宙刘,能夠及時(shí)地被其他線程看到。

共享變量:如果一個(gè)變量在多個(gè)線程的工作內(nèi)存中都存在副本杠览,那么這個(gè)變量就是這幾個(gè)線程的共享變量写烤。

JMM(Java內(nèi)存模型 - Java Memory Model) :描述了Java程序中各種變量(線程共享變量)的訪問(wèn)規(guī)則撒强,以及在JVM中將變量存儲(chǔ)到內(nèi)存和從內(nèi)存中讀取出變量這樣的底層細(xì)節(jié)。

  • 所有的變量都存儲(chǔ)在主內(nèi)存中
  • 每個(gè)線程都有自己獨(dú)立的工作內(nèi)存笙什,里面保存該線程使用到的變量的副本(主內(nèi)存中該變量的一份拷貝)
006.png
  • 線程對(duì)共享變量的所有操作都必須在自己的工作內(nèi)存中進(jìn)行,不能直接從主內(nèi)存中讀取
  • 不同線程之間無(wú)法直接訪問(wèn)其他線程工作內(nèi)存中的變量胚想,線程間變量值的傳遞需要通過(guò)主內(nèi)存來(lái)完成

共享變量可見(jiàn)性實(shí)現(xiàn)的原理

線程1對(duì)共享變量的修改要想被線程2及時(shí)看到琐凭,必須要經(jīng)過(guò)如下2個(gè)步驟:

  • 把工作內(nèi)存1中更新過(guò)的共享變量刷新到主內(nèi)存中
  • 將主內(nèi)存中最新的共享變量的值更新到工作內(nèi)存2中

練習(xí)題:

A、通過(guò)synchronized和volatile都可以實(shí)現(xiàn)可見(jiàn)性 √
B浊服、不同線程之間可以直接訪問(wèn)其他線程工作內(nèi)存中的變量 ×
C统屈、線程對(duì)共享變量的所有操作都必須在自己的工作內(nèi)存中進(jìn)行 √
D、所有的變量都存儲(chǔ)在主內(nèi)存中 √

要實(shí)現(xiàn)共享變量的可見(jiàn)性牙躺,必須保證兩點(diǎn):

  • 線程修改后的共享變量值能夠及時(shí)從工作內(nèi)存刷新主內(nèi)存中
  • 其他線程能夠及時(shí)把共享變量的最新值從主內(nèi)存更新到自己的工作內(nèi)存中

Java語(yǔ)言層面支持的可見(jiàn)性實(shí)現(xiàn)方式:

  • synchronized
  • volatile

synchronized能夠?qū)崿F(xiàn)

  • 原子性(通過(guò)同步實(shí)現(xiàn))
  • 內(nèi)存可見(jiàn)性

JMM關(guān)于synchronized的兩條規(guī)定:

  • 線程解鎖前愁憔,必須把共享變量的最新值刷新到主內(nèi)存中
  • 線程加鎖時(shí),將清空工作內(nèi)存中共享變量的值孽拷,從而使用共享變量時(shí)需要從主內(nèi)存中重新讀取最新的值(注意:加鎖與解鎖需要是同一把鎖)

線程解鎖前對(duì)共享變量的修改在下一次加鎖時(shí)對(duì)其他線程可見(jiàn)

線程執(zhí)行互斥代碼的過(guò)程:

  • 1吨掌、獲得互斥鎖
  • 2、清空工作內(nèi)存
  • 3脓恕、從主內(nèi)存拷貝變量的最新副本到工作內(nèi)存
  • 4膜宋、執(zhí)行代碼
  • 5、將更改后的共享變量的值刷新到主內(nèi)存
  • 6炼幔、釋放互斥鎖

重排序:代碼書(shū)寫(xiě)的順序與實(shí)際執(zhí)行的順序不同秋茫,指令重排序是編譯器或處理器為了提高程序性能而做的優(yōu)化
1、編譯器優(yōu)化的重排序(編譯器優(yōu)化)
2乃秀、指令集并行重排序(處理器優(yōu)化)
3肛著、內(nèi)存系統(tǒng)的重排序(處理器優(yōu)化)

as-if-serial:無(wú)論如何重排序,程序執(zhí)行的結(jié)果應(yīng)該與代碼順序執(zhí)行的結(jié)果一致(Java編譯器跺讯、運(yùn)行時(shí)和處理器都會(huì)保證Java在單線程下遵循as-if-serial語(yǔ)義)

int num1 = 1 ;      // 第1行代碼
int num2 = 2 ;      // 第2行代碼
int sum = num1 + num2 ;  // 第3行代碼

單線程:第1枢贿、2行的順序可以重排,但第3行不能
重排序不會(huì)給單線程帶來(lái)內(nèi)存可見(jiàn)性問(wèn)題
多線程中程序交錯(cuò)執(zhí)行時(shí)抬吟,重排序可能會(huì)造成內(nèi)存可見(jiàn)性問(wèn)題

public class SynchronizedDemo {
    // 共享變量
    private boolean ready = false;
    private int result = 0;
    private int number = 1;

    // 寫(xiě)操作
    public void write() {
        ready = true; // 1.1
        number = 2; // 1.2
    }

    // 讀操作
    public void read() {
        if (ready) { // 2.1
            result = number * 3; // 2.2
        }
        System.out.println("result的值為:" + result);
    }

    // 內(nèi)部線程類(lèi)
    private class ReadWriteThread extends Thread {
        // 根據(jù)構(gòu)造方法中傳入的flag參數(shù)萨咕,確定線程執(zhí)行讀操作還是寫(xiě)操作
        private boolean flag;

        public ReadWriteThread(boolean flag) {
            this.flag = flag;
        }

        @Override
        public void run() {
            if (flag) {
                // 構(gòu)造方法中傳入true,執(zhí)行寫(xiě)操作
                write();
            } else {
                // 構(gòu)造方法中傳入false火本,執(zhí)行讀操作
                read();
            }
        }
    }

    public static void main(String[] args) {
        // 啟動(dòng)線程執(zhí)行寫(xiě)操作
        SynchronizedDemo synDemo = new SynchronizedDemo();
        synDemo.new ReadWriteThread(true).start();
        
        // 程序并不牽涉線程交叉執(zhí)行的問(wèn)題危队,加入synchronized關(guān)鍵詞result也有可能為0,加入休眠操作钙畔,等主線程蘇醒茫陆,寫(xiě)線程基本執(zhí)行完,所以主線程繼續(xù)往下執(zhí)行擎析,啟動(dòng)讀線程簿盅』酉拢基本可以保證結(jié)果為6。
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        // 啟動(dòng)線程執(zhí)行讀操作
        synDemo.new ReadWriteThread(false).start();

    }
}

結(jié)果可能出現(xiàn):
result的值為:0
result的值為:6

導(dǎo)致共享變量在線程間不可見(jiàn)的原因:
1桨醋、線程的交叉執(zhí)行
2棚瘟、重排序結(jié)合線程交叉執(zhí)行
3、共享變量更新后的值沒(méi)有在工作內(nèi)存與主內(nèi)存間及時(shí)更新

synchronized解決方案:
1喜最、原子性保證:線程的交叉執(zhí)行
2偎蘸、原子性保證:重排序結(jié)合線程交叉執(zhí)行
3、可見(jiàn)性保證:共享變量更新后的值沒(méi)有在工作內(nèi)存與主內(nèi)存間及時(shí)更新

練習(xí)題:
A瞬内、當(dāng)兩個(gè)并發(fā)線程訪問(wèn)同一個(gè)對(duì)象object中的這個(gè)synchronized(this)同步代碼塊時(shí)迷雪,一個(gè)時(shí)間內(nèi)只能有一個(gè)線程得到執(zhí)行〕娴√
B章咧、當(dāng)一個(gè)線程訪問(wèn)object的一個(gè)synchronized(this)同步代碼塊時(shí),另一個(gè)線程仍然可以訪問(wèn)該object中的非synchronized(this)同步代碼塊能真。 √
C赁严、當(dāng)一個(gè)線程訪問(wèn)object的一個(gè)synchronized(this)同步代碼塊時(shí),其他線程對(duì)object中所有其它synchronized(this)同步代碼塊的訪問(wèn)不會(huì)被阻塞粉铐。 ×
D误澳、當(dāng)一個(gè)線程訪問(wèn)object的一個(gè)synchronized(this)同步代碼塊時(shí),它就獲得了這個(gè)object的對(duì)象鎖秦躯。結(jié)果忆谓,其它線程對(duì)該object對(duì)象所有同步代碼部分的訪問(wèn)都被暫時(shí)阻塞。 √

volatile如何實(shí)現(xiàn)內(nèi)存可見(jiàn)性:
深入來(lái)說(shuō):通過(guò)加入內(nèi)存屏障和禁止重排序優(yōu)化來(lái)實(shí)現(xiàn)的踱承。

  • 對(duì)volatile變量執(zhí)行寫(xiě)操作時(shí)倡缠,會(huì)在寫(xiě)操作后加入一條store屏障指令
  • 對(duì)volatile變量執(zhí)行讀操作時(shí),會(huì)在寫(xiě)操作前加入一條load屏障指令
    通俗的講:volatile變量在每次被線程訪問(wèn)時(shí)茎活,都強(qiáng)迫從主內(nèi)存中重讀該變量的值昙沦,而當(dāng)該變量發(fā)生變化時(shí),又會(huì)強(qiáng)迫線程將最新的值刷新到主內(nèi)存载荔。這樣任何時(shí)刻盾饮,不同的線程總能看到該變量的最新值。

線程寫(xiě)volatile變量的過(guò)程:
1懒熙、改變線程工作內(nèi)存中volatile變量副本的值
2丘损、將改變后的副本的值從工作內(nèi)存刷新到主內(nèi)存中

線程讀volatile變量的過(guò)程:
1、從主內(nèi)存中讀取volatile變量的最新值到線程的工作內(nèi)存中
2工扎、從工作內(nèi)存中讀取volatile變量的副本

volatile不能保證volatile變量復(fù)合操作的原子性:

private int number = 0 ;
number ++ ;     // 不是原子性

1徘钥、 讀取number的值
2、 將number的值加1
3肢娘、 寫(xiě)入最新的number的值

synchronized(this){
    number ++ ;
}

加入synchronized呈础,變?yōu)樵硬僮?/p>

private volatile int number = 0 ;

變?yōu)関olatile變量舆驶,無(wú)法保證原子性

public class VolatileDemo {

    private volatile int number = 0;
    
    public int getNumber(){
        return this.number;
    }
    
    public void increase(){
        try {
            Thread.sleep(100);      // 更多出現(xiàn)小于500的現(xiàn)象
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        number++;
    }
    
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        final VolatileDemo volDemo = new VolatileDemo();
        for(int i = 0 ; i < 500 ; i++){
            new Thread(new Runnable() {
                
                @Override
                public void run() {
                    volDemo.increase();
                }
            }).start();
        }
        
        //如果還有子線程在運(yùn)行,主線程就讓出CPU資源而钞,直到所有的子線程都運(yùn)行完了沙廉,主線程再繼續(xù)往下執(zhí)行
        while(Thread.activeCount() > 1){
            Thread.yield();
        }
        
        System.out.println("number : " + volDemo.getNumber());
    }

}

結(jié)果出現(xiàn):number:494

007.png
008.png
009.png
010.png

synchronized實(shí)現(xiàn)number變量的原子性

public class VolatileDemo {

    private int number = 0;
    
    public int getNumber(){
        return this.number;
    }
    
    public void increase(){
        try {
            Thread.sleep(100);  // 更多出現(xiàn)小于500的現(xiàn)象
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        
        synchronized(this) {  // 放在increase()方法前,由于休眠臼节,程序可能需要執(zhí)行很長(zhǎng)時(shí)間蓝仲,這里縮小鎖粒度,則利用此寫(xiě)法synchronized不僅保證了number變量的可見(jiàn)性官疲,還保證了number++的原子性
            number++;
        }
    }
    
    /**
     * @param args
     */
    public static void main(String[] args) {

        final VolatileDemo volDemo = new VolatileDemo();
        for(int i = 0 ; i < 500 ; i++){
            new Thread(new Runnable() {
                
                @Override
                public void run() {
                    volDemo.increase();
                }
            }).start();
        }
        
        while(Thread.activeCount() > 1){
            Thread.yield();
        }
        
        System.out.println("number : " + volDemo.getNumber());
    }

}

ReentrantLock實(shí)現(xiàn)number變量在線程中的原子性

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class VolatileDemo {

    private Lock lock = new ReentrantLock();  // 用ReentrantLock實(shí)現(xiàn)number變量在線程中的原子性
    private int number = 0;
    
    public int getNumber(){
        return this.number;
    }
    
    public void increase(){
        try {
            Thread.sleep(100);  // 更多出現(xiàn)小于500的現(xiàn)象
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        
        lock.lock();  // 加鎖 - 相當(dāng)于進(jìn)入synchronized代碼塊
        try {
            this.number++;
        } finally { // 鎖內(nèi)部操作可能會(huì)拋出異常,為保證鎖一定被釋放
            lock.unlock();  // 釋放鎖 - 相當(dāng)于退出synchronized代碼塊
        }
    }
    
    /**
     * @param args
     */
    public static void main(String[] args) {

        final VolatileDemo volDemo = new VolatileDemo();
        for(int i = 0 ; i < 500 ; i++){
            new Thread(new Runnable() {
                
                @Override
                public void run() {
                    volDemo.increase();
                }
            }).start();
        }
        
        while(Thread.activeCount() > 1){
            Thread.yield();
        }
        
        System.out.println("number : " + volDemo.getNumber());
    }

}

volatile適用場(chǎng)合:
要在多線程中安全的使用volatile變量亮隙,必須同時(shí)滿足:
1途凫、對(duì)變量的寫(xiě)入操作不依賴(lài)其當(dāng)前值

  • 不滿足:number++ 、 count = count * 5
  • 滿足:boolean變量溢吻、記錄溫度變化的變量等

2维费、該變量沒(méi)有包含在具體其他變量的不變式中

  • 不滿足:不變式 low < up

  • volatile不需要加鎖,比synchronized更輕量級(jí)促王,不會(huì)阻塞線程犀盟;

  • 從內(nèi)存可見(jiàn)性角度講,volatile讀相當(dāng)于加鎖蝇狼,volatile寫(xiě)相當(dāng)于解鎖阅畴;

  • synchronized既能保證可見(jiàn)性,又能保證原子性迅耘,而volatile只能保證可見(jiàn)性贱枣,無(wú)法保證原子性。

A volatile是保證被修飾變量的可見(jiàn)性颤专,同時(shí)也保證原子操作 ×
B Java中沒(méi)有提供檢測(cè)與避免死鎖的專(zhuān)門(mén)機(jī)制纽哥,但應(yīng)用程序員可以采用某些策略防止死鎖的發(fā)生 √
C JAVA中對(duì)共享數(shù)據(jù)操作的并發(fā)控制是采用加鎖技術(shù) √
D 共享數(shù)據(jù)的訪問(wèn)權(quán)限都必須定義為private √

  • final也可以保證內(nèi)存可見(jiàn)性

問(wèn):即使沒(méi)有保證可見(jiàn)性的措施,很多時(shí)候共享變量依然能夠在主內(nèi)存和工作內(nèi)存間得到即使的更新栖秕?

答:一般只有在短時(shí)間內(nèi)高并發(fā)的情況下才會(huì)出現(xiàn)變量得不到及時(shí)更新的情況春塌,因?yàn)镃PU在執(zhí)行時(shí)會(huì)很快地刷新緩存,所以一般情況下很難看到這種問(wèn)題簇捍。

對(duì)64位(long只壳、double)變量的讀寫(xiě)可能不是原子操作:

  • Java內(nèi)存模型允許JVM將沒(méi)有被volatile修飾的64位數(shù)據(jù)類(lèi)型的讀寫(xiě)操作劃分為兩次32位的讀寫(xiě)操作來(lái)進(jìn)行。

導(dǎo)致問(wèn)題:有可能會(huì)出現(xiàn)讀取到“半個(gè)變量”的情況
解決辦法:加volatile關(guān)鍵字

synchronized和volatile比較:

  • volatile比synchronized更輕量級(jí)
  • volatile沒(méi)有synchronized使用的廣泛
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末暑塑,一起剝皮案震驚了整個(gè)濱河市吕世,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌梯投,老刑警劉巖命辖,帶你破解...
    沈念sama閱讀 222,378評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件况毅,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡尔艇,警方通過(guò)查閱死者的電腦和手機(jī)尔许,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,970評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)终娃,“玉大人味廊,你說(shuō)我怎么就攤上這事√母” “怎么了余佛?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,983評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)窍荧。 經(jīng)常有香客問(wèn)我辉巡,道長(zhǎng),這世上最難降的妖魔是什么蕊退? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,938評(píng)論 1 299
  • 正文 為了忘掉前任郊楣,我火速辦了婚禮,結(jié)果婚禮上瓤荔,老公的妹妹穿的比我還像新娘净蚤。我一直安慰自己,他們只是感情好输硝,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,955評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布今瀑。 她就那樣靜靜地躺著,像睡著了一般点把。 火紅的嫁衣襯著肌膚如雪放椰。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,549評(píng)論 1 312
  • 那天愉粤,我揣著相機(jī)與錄音砾医,去河邊找鬼。 笑死衣厘,一個(gè)胖子當(dāng)著我的面吹牛如蚜,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播影暴,決...
    沈念sama閱讀 41,063評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼错邦,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了型宙?” 一聲冷哼從身側(cè)響起撬呢,我...
    開(kāi)封第一講書(shū)人閱讀 39,991評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎妆兑,沒(méi)想到半個(gè)月后魂拦,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體毛仪,經(jīng)...
    沈念sama閱讀 46,522評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,604評(píng)論 3 342
  • 正文 我和宋清朗相戀三年芯勘,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了箱靴。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,742評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡荷愕,死狀恐怖衡怀,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情安疗,我是刑警寧澤抛杨,帶...
    沈念sama閱讀 36,413評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站荐类,受9級(jí)特大地震影響怖现,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜掉冶,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,094評(píng)論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望脐雪。 院中可真熱鬧厌小,春花似錦、人聲如沸战秋。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,572評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)脂信。三九已至癣蟋,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間狰闪,已是汗流浹背疯搅。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,671評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留埋泵,地道東北人幔欧。 一個(gè)月前我還...
    沈念sama閱讀 49,159評(píng)論 3 378
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像丽声,于是被迫代替她去往敵國(guó)和親礁蔗。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,747評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容

  • 背景:多線程 可見(jiàn)性: 一個(gè)線程對(duì)共享變量的修改雁社,能夠及時(shí)地被其他線程看到浴井。共享變量: 如果一個(gè)變量在多個(gè)線程的工...
    zheting閱讀 196評(píng)論 0 0
  • 可見(jiàn)性: 一個(gè)線程對(duì)共享變量值的修改,能夠及時(shí)地被其他線程看到霉撵。 共享變量: 如果一個(gè)變量在多個(gè)線程的工作內(nèi)存中都...
    OmaiMoon閱讀 203評(píng)論 0 0
  • 以上代碼會(huì)重復(fù)運(yùn)行 磺浙, 不會(huì)停止洪囤。 JMM(java內(nèi)存模型) 若想學(xué)習(xí)好多線程, 那么必須了解一下JMM Jav...
    尼爾君閱讀 1,754評(píng)論 0 2
  • 除了充分利用計(jì)算機(jī)處理器的能力外屠缭,一個(gè)服務(wù)端同時(shí)對(duì)多個(gè)客戶端提供服務(wù)則是另一個(gè)更具體的并發(fā)應(yīng)用場(chǎng)景箍鼓。衡量一個(gè)服務(wù)性...
    胡二囧閱讀 1,347評(píng)論 0 12
  • 可見(jiàn)性 可見(jiàn)性:一個(gè)線程對(duì)共享變量值的修改,能夠及時(shí)地被其他線程看到呵曹。共享變量:如果一個(gè)變量在多個(gè)線程的工作內(nèi)存中...
    Java_Explorer閱讀 1,105評(píng)論 0 0