java 同步的三種方式:volatile、鎖省有、final

轉(zhuǎn)載:http://www.cnblogs.com/leesf456/p/5291484.html


1痒留、volatile

1.1 介紹

關鍵字volatile是Java虛擬機提供的最輕量級的同步機制。
當一個變量定義為volatile時蠢沿,它將具備兩種特性:(1)可見性伸头;(2)禁止指令重排序。

  1. 可見性
    當一條線程修改了這個變量的值舷蟀,新值對于其他線程來說是可以立即獲得的恤磷,但是基于volatile變量的操作并不是安全的(如自增操作),不能保證原子性野宜。
  2. 禁止指令重排序
    不允許對volatile操作指令進行重排序扫步。

1.2 volatile 的 happens - before 關系

在volatile變量與happens - before 之間是什么關系呢,我們通過一個示例說明:

class VolatileExample {
    int a = 0;
    volatile boolean flag = false;

    public void writer() {
        a = 1;          //1
        flag = true;    //2
    }

    public void reader() {
        if (flag) {         //3
            int i = a;     //4
        }
    }
}

說明:假定線程A先執(zhí)行writer方法匈子,線程B后執(zhí)行reader方法河胎,那么根據(jù)happens - before關系,我們可以知道:

  1. 根據(jù)程序順序規(guī)則虎敦,1 happens before 2; 3 happens before 4游岳。
  2. 根據(jù) volatile變量規(guī)則政敢,2 happens before 3。
  3. 根據(jù) happens before 的傳遞性胚迫,1 happens before 4喷户。

具體的happens - before圖形化如下:


這里寫圖片描述

1.3 volatile 讀寫內(nèi)存語義

  1. 讀內(nèi)存語義。當讀一個 volatile 變量時晌区,JMM 會把該線程對應的本地內(nèi)存置為無效。線程之后將從主內(nèi)存中讀取共享變量通贞。

  2. 寫內(nèi)存語義朗若。當寫一個 volatile 變量時,JMM 會把該線程對應的本地內(nèi)存中的共享變量值刷新到主內(nèi)存昌罩。這樣就保證了volatile的內(nèi)存可見性哭懈。

volatile讀寫內(nèi)存語義總結為如下三條:

  1. 線程 A 寫一個 volatile 變量,實質(zhì)上是線程 A 向接下來將要讀這個 volatile 變量的某個線程發(fā)出了(其對共享變量所在修改的)消息茎用。

  2. 線程 B 讀一個 volatile 變量遣总,實質(zhì)上是線程 B 接收了之前某個線程發(fā)出的(在寫這個 volatile 變量之前對共享變量所做修改的)消息。

  3. 線程 A 寫一個 volatile 變量轨功,隨后線程 B 讀這個 volatile 變量旭斥,這個過程實質(zhì)上是線程 A 通過主內(nèi)存向線程 B 發(fā)送消息。

1.4 volatile 內(nèi)存語義的實現(xiàn)

前面講到古涧,volatile變量會禁止編譯器垂券、處理器重排序。下面是volatile具體的排序規(guī)則表:


這里寫圖片描述

為了實現(xiàn) volatile 的內(nèi)存語義羡滑,編譯器在生成字節(jié)碼時菇爪,會在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序。對于編譯器來說柒昏,發(fā)現(xiàn)一個最優(yōu)布置來最小化插入屏障的總數(shù)幾乎不可能凳宙,為此,JMM 采取保守策略职祷。下面是基于保守策略的 JMM 內(nèi)存屏障插入策略:

  1. 在每個 volatile 寫操作的前面插入一個 StoreStore 屏障氏涩。

  2. 在每個 volatile 寫操作的后面插入一個 StoreLoad 屏障(對volatile寫、普通讀寫實現(xiàn)為不允許重排序有梆,可能會影響性能)削葱。

  3. 在每個 volatile 讀操作的后面插入一個 LoadLoad 屏障。

  4. 在每個 volatile 讀操作的后面插入一個 LoadStore 屏障(普通讀寫淳梦、volatile讀實現(xiàn)為不允許重排序析砸,可能會影響性能)。

下面通過一個示例展示volatile的內(nèi)存語義:

class VolatileBarrierExample { 
    int a;
    volatile int v1 = 1;
    volatile int v2 = 2;

    void readAndWrite() {
        int i = v1;      // 第一個 volatile 讀
        int j = v2;      // 第二個 volatile 讀
        a = i + j;       // 普通寫
        v1 = i + 1;      // 第一個 volatile 寫
        v2 = j * 2;      // 第二個 volatile 寫
    }
}

根據(jù)程序和插入屏障的規(guī)則爆袍,最后的指令序列如下圖所示:


這里寫圖片描述

說明:編譯器首繁、處理器會根據(jù)上下文進行優(yōu)化作郭,并不是完全按照保守策略進行插入相應的屏障指令。


2弦疮、鎖

2.1 介紹

鎖是Java并發(fā)編程中最重要的同步機制夹攒。
鎖除了讓臨界區(qū)互斥執(zhí)行外,還可以讓釋放鎖的線程向獲取同一個鎖的線程發(fā)送消息胁塞。

2.2 鎖的 happens - before 關系

下面一個示例展示了鎖的使用:

class MonitorExample {
    int a = 0;

    public synchronized void writer() {    // 1 
        a++;     // 2
    }               // 3

    public synchronized void reader() { // 4 
        int i = a;    // 5
    }                   // 6
}

說明:假設線程 A 執(zhí)行 writer()方法咏尝,隨后線程 B 執(zhí)行 reader()方法江掩。該程序的happens - before關系如下:

  1. 根據(jù)程序順序規(guī)則放仗,1 happens before 2, 2 happens before 3; 4 happens before 5, 5 happens before 6郭蕉。

  2. 根據(jù)監(jiān)視器鎖規(guī)則酒觅,3 happens before 4危队。

  3. 根據(jù)傳遞性谊惭,2 happens before 5空骚。

圖形化表示如下:


這里寫圖片描述

2.3 鎖的內(nèi)存語義

1. 當線程釋放鎖時叠国,JMM會把該線程對應的工作內(nèi)存中的共享變量刷新到主內(nèi)存中衩匣,以確保之后的線程可以獲取到最新的值蕾总。

2. 當線程獲取鎖時,JMM會把該線程對應的本地內(nèi)存置為無效琅捏。從而使得被監(jiān)視器保護的臨界區(qū)代碼必須要從主內(nèi)存中去讀取共享變量生百。

鎖的釋放與獲取總結為如下三條:

  1. 線程 A 釋放一個鎖,實質(zhì)上是線程 A 向接下來將要獲取這個鎖的某個線程發(fā)出了(線程 A 對共享變量所做修改的)消息柄延。

  2. 線程 B 獲取一個鎖置侍,實質(zhì)上是線程 B 接收了之前某個線程發(fā)出的(在釋放這個鎖之前對共享變量所做修改的)消息。

  3. 線程 A 釋放鎖拦焚,隨后線程 B 獲取這個鎖蜡坊,這個過程實質(zhì)上是線程 A 通過主內(nèi)存向線程 B 發(fā)送消息。

2.4 鎖內(nèi)存語義的實現(xiàn)

鎖的內(nèi)存語義的具體實現(xiàn)借助了volatile變量的內(nèi)存語義的實現(xiàn)赎败。


3.final

3.1 介紹

對于 final 域秕衙,編譯器和處理器要遵守兩個重排序規(guī)則:

  1. final 寫:“構造函數(shù)內(nèi)對一個final域的寫入”,與“隨后把這個被構造對象的引用賦值給一個引用變量”僵刮,這兩個操作之間不能重排序据忘。

  2. final 讀:“初次讀一個包含final域的對象的引用”,與“隨后初次讀對象的final域”搞糕,這兩個操作之間不能重排序勇吊。

如下面示例展示了final兩種重排序規(guī)則:

public final class FinalExample {
    final int i;
    public FinalExample() {
        i = 3;     // 1
    }
    
    public static void main(String[] args) {
        FinalExample fe = new FinalExample();    // 2
        int ele = fe.i;                          // 3
    }
}

說明: 操作1與操作2符合重排序規(guī)則1,不能重排窍仰,操作2與操作3符合重排序規(guī)則2汉规,不能重排。

由下面的示例我們來具體理解final域的重排序規(guī)則。

public class FinalExample {
    int i;                      // 普通變量
    final int j;                // final變量
    static FinalExample obj;    // 靜態(tài)變量

    public void FinalExample () { // 構造函數(shù) 
        i = 1;    // 寫普通域
        j = 2;    // 寫final域
    }

    public static void writer () { // 寫線程A執(zhí)行 
        obj = new FinalExample();
    }

    public static void reader () {   // 讀線程B執(zhí)行
        FinalExample object = obj;   // 讀對象引用
        int a = object.i;            // 讀普通域
        int b = object.j;            // 讀final域
    }
}

說明:假設線程A先執(zhí)行writer()方法针史,隨后另一個線程B執(zhí)行reader()方法晶伦。下面我們通過這兩個線程的交互來說明這兩個規(guī)則。

3.2 寫 final 域 重排序規(guī)則

寫 final 域的重排序規(guī)則禁止把 final 域的寫重排序到構造函數(shù)之外啄枕。這個規(guī)則的實現(xiàn)包含下面兩個方面:

  1. JMM 禁止編譯器把 final 域的寫重排序到構造函數(shù)之外婚陪。

  2. 編譯器會在 final 域的寫之后,構造函數(shù) return 之前频祝,插入一個 StoreStore 屏障泌参。這個屏障禁止處理器把 final 域的寫重排序到構造函數(shù)之外。

writer方法的obj = new FinalExample();其實包括兩步常空,首先是在堆上分配一塊內(nèi)存空間創(chuàng)建FinalExample對象沽一,然后將這個對象的地址賦值給obj引用。假設線程 B 讀對象引用與讀對象的成員域之間沒有重排序窟绷,則可能的時序圖如下:


這里寫圖片描述

說明:寫普通域的操作被編譯器重排序到了構造函數(shù)之外锯玛,讀線程 B 錯誤的讀取了普通變量 i 初始化之前的值咐柜。而寫 final 域的操作兼蜈,被寫 final 域的重排序規(guī)則 “限定”在了構造函數(shù)之內(nèi),讀線程 B 正確的讀取了 final 變量初始化之后的值拙友。寫 final 域的重排序規(guī)則可以確保:在對象引用為任意線程可見之前为狸,對象的 final 域已經(jīng)被正確初始化過了,而普通域不具有這個保障遗契。以上圖為例辐棒,在讀線程 B “看到”對象引用 obj 時,很可能 obj 對象還沒有構造完成(對普通域 i 的寫操作被重排序到構造函數(shù)外牍蜂,此時初始值 2 還沒有寫入普通域 i)漾根。

3.3 讀 final 域 重排序規(guī)則

讀 final 域的重排序規(guī)則如下:

在一個線程中,"初次讀對象引用"與"初次讀該對象包含的 final 域"鲫竞,JMM 禁止處理器重排序這兩個操作(注意辐怕,這個規(guī)則僅僅針對處理器)。
編譯器會在讀 final 域操作的前面插入一個 LoadLoad 屏障从绘。
初次讀對象引用與初次讀該對象包含的 final 域寄疏,這兩個操作之間存在間接依賴關系。由于編譯器遵守間接依賴關系僵井,因此編譯器不會重排序這兩個操作陕截。大多數(shù)處理器也會遵守間接依賴,大多數(shù)處理器也不會重排序這兩個操作批什。但有少數(shù)處理器允許對存在間接依賴關系的操作做重排序(比如 alpha 處理器)农曲,這個規(guī)則就是專門用來針對這種處理器。

reader方法包含三個操作:① 初次讀引用變量 obj驻债。② 初次讀引用變量 obj 指向?qū)ο蟮钠胀ㄓ?i朋蔫。③ 初次讀引用變量 obj 指向?qū)ο蟮?final 域 j罚渐。

假設寫線程 A 沒有發(fā)生任何重排序,同時程序在不遵守間接依賴的處理器上執(zhí)行驯妄,下面是一種可能的執(zhí)行時序:


這里寫圖片描述

3.4 如果 final域 是引用類型

上面我們的例子中荷并,final域是基本數(shù)據(jù)類型,如果final與為引用類型的話情況會稍微不同青扔。對于引用類型源织,寫 final 域的重排序規(guī)則對編譯器和處理器增加了如下約束:

在構造函數(shù)內(nèi)對一個 final 引用的對象的成員域的寫入,與隨后在構造函數(shù)外把這個被構造對象的引用賦值給一個引用變量微猖,這兩個操作之間不能重排序谈息。

3.5 final 逸出

寫 final 域 的重排序規(guī)則可以確保:

在引用變量為任意線程可見之前,該引用變量指向的對象的 final 域已經(jīng)在構造函數(shù)中被正確初始化過了凛剥。

其實要得到這個效果侠仇,還需要一個保證:

在構造函數(shù)內(nèi)部,不能讓這個被構造對象的引用為其他線程可見犁珠,也就是對象引用不能在構造函數(shù)中“逸出”逻炊。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市犁享,隨后出現(xiàn)的幾起案子余素,更是在濱河造成了極大的恐慌,老刑警劉巖炊昆,帶你破解...
    沈念sama閱讀 222,946評論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件桨吊,死亡現(xiàn)場離奇詭異,居然都是意外死亡凤巨,警方通過查閱死者的電腦和手機视乐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,336評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來敢茁,“玉大人佑淀,你說我怎么就攤上這事【硪” “怎么了渣聚?”我有些...
    開封第一講書人閱讀 169,716評論 0 364
  • 文/不壞的土叔 我叫張陵,是天一觀的道長僧叉。 經(jīng)常有香客問我奕枝,道長,這世上最難降的妖魔是什么瓶堕? 我笑而不...
    開封第一講書人閱讀 60,222評論 1 300
  • 正文 為了忘掉前任隘道,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘谭梗。我一直安慰自己忘晤,他們只是感情好,可當我...
    茶點故事閱讀 69,223評論 6 398
  • 文/花漫 我一把揭開白布激捏。 她就那樣靜靜地躺著设塔,像睡著了一般。 火紅的嫁衣襯著肌膚如雪远舅。 梳的紋絲不亂的頭發(fā)上闰蛔,一...
    開封第一講書人閱讀 52,807評論 1 314
  • 那天,我揣著相機與錄音图柏,去河邊找鬼序六。 笑死,一個胖子當著我的面吹牛蚤吹,可吹牛的內(nèi)容都是我干的例诀。 我是一名探鬼主播,決...
    沈念sama閱讀 41,235評論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼裁着,長吁一口氣:“原來是場噩夢啊……” “哼繁涂!你這毒婦竟也來了?” 一聲冷哼從身側響起跨算,我...
    開封第一講書人閱讀 40,189評論 0 277
  • 序言:老撾萬榮一對情侶失蹤爆土,失蹤者是張志新(化名)和其女友劉穎椭懊,沒想到半個月后诸蚕,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,712評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡氧猬,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,775評論 3 343
  • 正文 我和宋清朗相戀三年背犯,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片盅抚。...
    茶點故事閱讀 40,926評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡漠魏,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出妄均,到底是詐尸還是另有隱情柱锹,我是刑警寧澤,帶...
    沈念sama閱讀 36,580評論 5 351
  • 正文 年R本政府宣布丰包,位于F島的核電站禁熏,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏邑彪。R本人自食惡果不足惜瞧毙,卻給世界環(huán)境...
    茶點故事閱讀 42,259評論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧宙彪,春花似錦矩动、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,750評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至男图,卻和暖如春檀训,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背享言。 一陣腳步聲響...
    開封第一講書人閱讀 33,867評論 1 274
  • 我被黑心中介騙來泰國打工峻凫, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人览露。 一個月前我還...
    沈念sama閱讀 49,368評論 3 379
  • 正文 我出身青樓荧琼,卻偏偏與公主長得像,于是被迫代替她去往敵國和親差牛。 傳聞我的和親對象是個殘疾皇子命锄,可洞房花燭夜當晚...
    茶點故事閱讀 45,930評論 2 361

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