Java學(xué)習(xí)內(nèi)存模型以及線程安全的可見性問題(八)

上次線程池已經(jīng)說過了,從今天開始一起了解下JVM內(nèi)存模型詳解。

(一)容易誤解的部分

老鐵很容易把JAVA的內(nèi)存區(qū)域、JAVA的內(nèi)存模型,GC分代回收的老年代和新生代也容易搞混,繞進(jìn)去繞不出來。學(xué)習(xí)多線程之前一定要搞明白這些問題,可能在你的內(nèi)心一直認(rèn)為多線程就是一個(gè)工具,所有的底層都是C++來寫的,沒辦法去看,為什么要有java,java其實(shí)就是屏蔽了底層的復(fù)雜性。

  • ① GC內(nèi)存區(qū)域

堆的概念,老年代,新生代,Eden,S0,S1

  • ② JAVA的內(nèi)存區(qū)域

JVM運(yùn)行時(shí)的區(qū)域:java編譯生成class,線程共享部分(方法區(qū),堆內(nèi)存),線程獨(dú)占部分(虛擬機(jī)棧,本地方法棧,程序計(jì)數(shù)器)

  • ③ JAVA的內(nèi)存模型(概念)

針對(duì)多核多CPU,多線程而制定的一套規(guī)范規(guī)則,不是一種開發(fā)技術(shù)。

(二)多線程中的問題

  1. 所見非所得(你看到的并不是所想的)、
  2. 無法肉眼去檢測(cè)程序的準(zhǔn)確性(多線程下,完全看不出來正常不正常)。
  3. 不同的運(yùn)行平臺(tái)有不同的表現(xiàn)。
  4. 錯(cuò)誤很難重現(xiàn)。

(三)工作內(nèi)存和主內(nèi)存

  • ① 主內(nèi)存

創(chuàng)建一個(gè)對(duì)象在堆里面,也可以稱之為主內(nèi)存,不僅僅是在堆,存在一個(gè)對(duì)象X,就存在主內(nèi)存

  • ② 工作內(nèi)存

線程運(yùn)行在工作內(nèi)存, 虛擬機(jī)棧,程序計(jì)數(shù)器,CPU,高速緩存。

工作內(nèi)存和主內(nèi)存只是一個(gè)邏輯上的劃分,概念上的東西。

  • ③ 奇妙的現(xiàn)象

主內(nèi)存的flag傳輸?shù)焦ぷ鲀?nèi)存flag的時(shí)候,存在CPU緩存的情況,CPU緩存可能導(dǎo)致非常短的時(shí)間內(nèi)不一致,本身CPU廠家底層是要做一致處理的,但是存在短時(shí)間內(nèi)的不一致。

(四)指令重排

  • ① 介紹

Java語言規(guī)范JVM線程內(nèi)部維持順序或語義,即只要程序的最終結(jié)果與它順序化情況的結(jié)果相等,那么指令的執(zhí)行順序可以與代碼邏輯順序不一致,這個(gè)過程就叫做指令的重排序。

  • ② 意義

使指令更加符合CPU的執(zhí)行特性,最大限度的發(fā)揮機(jī)器的性能,提高程序的執(zhí)行效率。

重排序,只能保證單個(gè)線程的,如果是多線程的話,就沒有爆發(fā)保證重排序。

// 線程1 
a = d; b = 2
// 線程2 
c = a; d =3

//重排序后
//線程1 
b = 2 ; a =d;
//線程2
d = 3 ; c =a;

編譯器和處理器可能會(huì)對(duì)操作做重排序。編譯器和處理器在重排序時(shí),會(huì)遵守?cái)?shù)據(jù)依賴性,編譯器和處理器不會(huì)改變存在數(shù)據(jù)依賴關(guān)系的兩個(gè)操作的執(zhí)行順序。

(五) 如何不進(jìn)行指令排序

  • ① 介紹

The Java volatile keyword is used to mark a Java variable as "being stored in main memory". More precisely that means, that every read of a volatile variable will be read from the computer's main memory, and not from the CPU cache, and that every write to a volatile variable will be written to main memory, and not just to the CPU cache。 CPU不緩存。

  • ② 實(shí)例
public class VisibilityDemo2 {
    // 狀態(tài)標(biāo)識(shí) (不用緩存)
    private volatile boolean flag = true;

    // 源碼 -> 字節(jié)碼class
    // JVM 轉(zhuǎn)化為 操作系統(tǒng)能夠執(zhí)行的代碼 (JIT Just In Time Compiler 編譯器 )(JVM  --  client   , --server)
    public static void main(String[] args) throws InterruptedException {
        VisibilityDemo2 demo1 = new VisibilityDemo2();
        new Thread(new Runnable() {
            public void run() {
                int i = 0;
                while (demo1.flag) {
                    i++;
                }
                System.out.println(i);
            }
        }).start();

        TimeUnit.SECONDS.sleep(2);
        // 設(shè)置is為false,使上面的線程結(jié)束while循環(huán)
        demo1.flag = false;
        System.out.println("被置為false了.");
    }
}

不添加volatile ,就不會(huì)打印i的值。

(六) 內(nèi)存模型

  • ① 介紹

內(nèi)存模型描述程序的可能行為。JAVA編程語言內(nèi)存模型通過檢查執(zhí)行跟蹤中的每個(gè)讀操作,并根據(jù)某些規(guī)則檢查該操作觀察到的寫操作是否有效來工作。

只要程序的所有執(zhí)行產(chǎn)生的結(jié)果都可以由內(nèi)存模型預(yù)測(cè),具體的實(shí)現(xiàn)者任意實(shí)現(xiàn),包括操作的重新排序和刪除不必要的同步。

內(nèi)存模型決定了在程序的每個(gè)點(diǎn)上可以讀取什么值。

  • ② 共享變量描述

可以在線程之間共享的內(nèi)存稱為共享內(nèi)存或堆內(nèi)存。所有實(shí)例字段,靜態(tài)字段和數(shù)組元素都存儲(chǔ)在堆內(nèi)存中。如果至少有一個(gè)訪問是寫的,那么對(duì)同一個(gè)變量的兩次訪問(讀或?qū)懀┦菦_突的。線程1修改過共享變量后,將共享變量刷到主內(nèi)存,然后,線程2從主內(nèi)存讀取該共享變量,將該共享變量載入到工作內(nèi)存中。

  • ③ 線程操作的定義
  1. write要寫的變量以及要寫的值。
  2. read 要讀的變量以及可見的寫入值(由此,我們可以確定可見的值)。
  3. lock 要鎖定的管程。
  4. unlock 要解鎖的管程。
  5. 外部操作(socket等等)。
  6. 啟動(dòng)和終止。

如果一個(gè)程序沒有數(shù)據(jù)競(jìng)爭(zhēng),那么程序的所有執(zhí)行看起來都是順序一致的。這是重排序必須要遵守的規(guī)則。

(七)對(duì)于同步規(guī)則的定義

  • ① 對(duì)于監(jiān)視器m的解鎖與所有后續(xù)操作對(duì)于m的加鎖同步

synchronized 在同步關(guān)鍵字,在內(nèi)存中都明確定義了,保持可見,及時(shí)反饋給主內(nèi)存中。一環(huán)扣一環(huán),想可見,必須反饋到主內(nèi)存。既有同步的語義,還有保持可見性的功能。


import java.util.concurrent.TimeUnit;

public class VisibilityDemo1 {
    // 狀態(tài)標(biāo)識(shí)
    private static boolean is = true;

    public static void main(String[] args) {
        new Thread(new Runnable() {
            public void run() {
                int i = 0;
                while (VisibilityDemo1.is) {
                    synchronized (this) {
                        i++;
                    }
                }
                System.out.println(i);
            }
        }).start();

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 設(shè)置is為false,使上面的線程結(jié)束while循環(huán)
        VisibilityDemo1.is = false;
        System.out.println("被置為false了.");
    }
}

  • ② 對(duì)volatile 變量v的寫入,與所有其他線程后續(xù)對(duì) v 的讀同步

變量標(biāo)識(shí)了volatile ,后面不管哪個(gè)線程來讀,都是同步的,都是可見的。

public class VisibilityDemo {
    private volatile boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        VisibilityDemo demo1 = new VisibilityDemo();
        Thread thread1 = new Thread(new Runnable() {
            public void run() {
                int i = 0;
                // class ->  運(yùn)行時(shí)jit編譯  -> 匯編指令 -> 重排序
                while (demo1.flag) { // 指令重排序
                    i++;
                }
                System.out.println(i);
            }
        });
        thread1.start();

        TimeUnit.SECONDS.sleep(2);
        // 設(shè)置is為false,使上面的線程結(jié)束while循環(huán)
        demo1.flag = false;
        System.out.println("被置為false了.");
    }
}

  • ③ 啟動(dòng)線程的操作與線程中的第一個(gè)操作同步
  • ④ 對(duì)于每個(gè)屬性寫入默認(rèn)值(0,false,null)與 每個(gè)線程對(duì)其操作的同步

  • ⑤ 線程T1的最后操作與線程T2發(fā)現(xiàn)線程T1已經(jīng)結(jié)束同步(isAlive,join可以判斷線程是否終結(jié))

  • ⑥ 如果線程T1終端了T2,那么線程T1的中斷操作與其他所有線程發(fā)現(xiàn)T2倍中斷了同步,通過拋出InterruptedException異常,或者調(diào)用Thread.interrupted 或者 Thread.isInterrupted。

(八)Happyens-before先行發(fā)生原則

  • ① 介紹

強(qiáng)調(diào)兩個(gè)有沖突的動(dòng)作之間的順序,以及定義數(shù)據(jù)征用的發(fā)生時(shí)機(jī)。

  • ② 原則
  1. 同一個(gè)線程里面對(duì)數(shù)據(jù)做了變動(dòng),后面的動(dòng)作可以及時(shí)的看到,其實(shí)還是可見性。
  2. 某個(gè)monitor上的unlock動(dòng)作 happens-before 同一個(gè)monitor上后續(xù)的lock動(dòng)作。
  3. 對(duì)某個(gè)volatile 字段的寫操作 happens-before 每個(gè)后續(xù)對(duì)該 volatile 字段的讀操作。
  4. 在某個(gè)線程對(duì)象上調(diào)用start() 方法 happens-before 該啟動(dòng)了的線程中的任意動(dòng)作。
  5. 某個(gè)線程中的所有動(dòng)作 happens-before 任意其他線程成功從該線程對(duì)象上的join() 中返回。
  6. 如果某個(gè)動(dòng)作 a 在happens-before 動(dòng)作 b,b 在happens-before 動(dòng)作 c,則 a happens-before c。

(九) final 在JMM中的處理

  • ① final在該對(duì)象的構(gòu)造函數(shù)中設(shè)置對(duì)象的字段,當(dāng)線程看到該對(duì)象時(shí),將始終看到該對(duì)象的final字段的正確構(gòu)造版本。
  • ② 如果在構(gòu)造函數(shù)中設(shè)置字段后發(fā)生讀取,則會(huì)看到該final字段分配的值,否則它將看到默認(rèn)值。

  • ③ 讀取該共享對(duì)象的final成員變量之前,先要讀取共享對(duì)象。

  • ④ 通常static final 是不可以修改的字段。然而System.in, System.out 和 System.err 是static final 字段,遺留原因,必須允許通過set方法改變,這些字段稱為寫保護(hù),以區(qū)別于普通的final字段。

PS:使用了volatile,unlock和lock的時(shí)候,就可以保證代碼不進(jìn)行重排序。內(nèi)存模型java進(jìn)階的一個(gè)核心點(diǎn),這個(gè)理解了,其實(shí)比寫多少年的業(yè)務(wù)代碼要重要很多。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,055評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評(píng)論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,346評(píng)論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,889評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評(píng)論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,118評(píng)論 0 286
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,637評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,558評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,739評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評(píng)論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,980評(píng)論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評(píng)論 1 280
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,347評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,702評(píng)論 2 370

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