導航:首頁 > 編程語言 > java並發編程教程

java並發編程教程

發布時間:2024-09-21 09:05:15

『壹』 我們一起學並發編程:Java內存模型(六)final的內存語義

上篇介紹了鎖和volatile的內存語義,本文講述的是final的內存語義,相比之下,final域的讀和寫更像是普通變數的訪問。

1、final域的重排序規則final

對於final域編譯器和處理器遵循兩個重排序規則

在構造函數內對一個final域的寫入,與隨後把這個對象的引用賦值給另一個引用變數,這兩個操作之間不能重排序

初次讀一個包含final域的對象的引用,與隨後初次讀這個final域,這兩個操作之間不能重排序。

讀起來相對拗口,用代碼來說明上面兩種重排序規則:

packagecom.lizba.p1;/***<p>**</p>**@Author:Liziba*@Date:2021/6/1120:37*/publicclassFinalExample{/**普通變數*/inti;/**final變數*/finalintj;/**對象引用*/staticFinalExampleobj;/***構造函數*/publicFinalExample(){//寫普通域this.i=1;//寫final域this.j=2;}/***線程A執行writer寫方法**/publicstaticvoidwriter(){obj=newFinalExample();}/***線程B執行reader讀方法**/publicstaticvoidreader(){//讀對象的引用FinalExamplefinalExample=obj;//讀普通域inta=finalExample.i;//讀final域intb=finalExample.j;}}

假設線程A執行writer()方法,線程B執行reader()方法。下面來通過這兩個線程的交互來說明這兩個規則。

2、寫final域的重排序規則

寫final域的重排序禁止吧final域的寫重排序到構造函數之外。通過如下方式來實現:

JMM禁止編譯器把final域的寫重排序到構造函數之外

編譯器會在final域的寫之後,構造函數return之前,插入一個StoreStore屏障。這個屏障禁止處理器把final域的寫重排序到構造函數之外。

現在開始分析writer()方法:

/***線程A執行writer寫方法**/publicstaticvoidwriter(){obj=newFinalExample();}

構造一個FinalExample類型的對象

將對象的引用賦值給變數obj

首先假設線程B讀對象引用與讀對象的成員域之間沒有重排序,則下圖是其一種執行可能

線程執行時序圖

3、讀final與的重排序規則

讀final域的重排序規則是,在一個線程中,初次讀對象引用與初次讀該對象包含的final域,JMM禁止處理器重排序這兩個操作(注意是處理器)。編譯器會在讀final域操作的前面插入一個LoadLoad屏障。

解釋:初次讀對象引用與初次讀該對象包含的final域,這兩個操作之間存在間接依賴關系。

編譯器遵守間接依賴關系,編譯器不會重排序這兩個操作

大多數處理器也遵守間接依賴,不會重排序這兩個操作。但是少部分處理器允許對存在間接依賴關系的操作做重排序(比如alpha處理器),這個規則就是專門針對這種處理器的。

分析reader()方法:

/***線程B執行reader讀方法**/publicstaticvoidreader(){//讀對象的引用FinalExamplefinalExample=obj;//讀普通域inta=finalExample.i;//讀final域intb=finalExample.j;}

初次讀引用變數obj

初次讀引用變數obj指向對象的普通域j

初次讀引用變數obj指向對象的final域i

假設B線程所處的處理器不遵守間接依賴關系,且A線程執行過程中沒有發生任何重排序,此時存在如下的執行時序:

線程執行時序圖

上圖B線程中讀對象的普通域被重排序到處理器讀取對象引用之前,此時普通域i還沒有被線程A寫入,因此這是一個錯誤的讀取操作。但是final域的讀取會被重排序規則把讀final域的操作「限定」在讀該final域所屬對象的引用讀取之後,此時final域已經被正確的初始化了,這是一個正確的讀取操作。

總結:

讀final域的重排序規則可以確保,在讀一個對象的final域之前,一定會先讀包含這個final域的對象的引用。

4、final域為引用類型

上面講述了基礎數據類型,如果final域修飾的引用類型又該如何?

packagecom.lizba.p1;/***<p>*final修飾引用類型變數*</p>**@Author:Liziba*@Date:2021/6/1121:52*/{/**final是引用類型*/finalint[]intArray;;/***構造函數*/publicFinalReferenceExample(){this.intArray=newint[1];//1intArray[0]=1;//2}/***寫線程A執行*/publicstaticvoidwriter1(){obj=newFinalReferenceExample();//3}/***寫線程B執行*/publicstaticvoidwriter2(){obj.intArray[0]=2;//4}/***讀線程C執行*/publicstaticvoidreader(){if(obj!=null){//5inttemp=obj.intArray[0];//6}}}

如上final域為一個int類型的數組的引用變數。對應引用類型,寫final域的重排序對編譯器和處理器增加了如下約束:

在構造函數內對一個final引用的對象的成員域的寫入,與隨後在構造函數外把這個被構造對象的引用賦值給另一個引用變數,這兩個操作不能重排序。

對於上述程序,假設A執行writer1()方法,執行完後線程B執行writer2()方法,執行完後線程C執行reader()方法。則存在如下線程執行時序:

引用型final的執行時序圖

JMM對於上述代碼,可以確保讀線程C至少能看到寫線程A在構造函數中對final引用對象的成員域的寫入。即寫線程C至少能看到數組下標0的值為1。但是寫線程B對數組元素的寫入,讀線程C可能看得到可能看不到。JMM不能保證線程B的寫入對讀線程C可見。因為寫線程B和讀線程C之間存在數據競爭,此時的執行結果不可預知。

此時如果想確保讀線程C看到寫線程B對數組元素的寫入,可以結合同步原語(volatile或者lock)來實現。

5、為什麼final引用不能從構造函數內「逸出」

本文一直在說寫final域的重排序規則可以確保:在引用變數為任意線程可見之前,該引用變數指向的對象的final域已經在構造函數中被正確初始化了。那究竟是如何實現的呢?

其實這需要另一個條件:在構造函數內部,不能讓這個被構造對象的引用被其它線程所見。也就是對象引用不能在構造函數中「逸出」。

示例代碼:

packagecom.lizba.p1;/***<p>*final引用逸出demo*</p>**@Author:Liziba*@Date:2021/6/1122:33*/{finalinti;;(){i=1;//1、寫final域obj=this;//2、this引用在此處"逸出"}publicstaticvoidwriter(){();}publicstaticvoidreader(){if(obj!=null){//3inttemp=obj.i;//4}}}

假設線程A執行writer()方法,線程B執行reader()方法。這里操作2導致對象還未完成構造前就對線程B可見了。因為1和2允許重排序,所以線程B可能無法看到final域被正確初始化後的值。實際執行的時序圖可能如下所示:

多線程執行時序圖

總結:

在構造函數返回之前,被構造對象的引用不能為其他線程可見,因為此時的final域可能還沒被初始化。而在構造函數返回後,任意線程都將保證能看到final域正確初始化之後的值。

6、final語義在處理器中的實現

舉例X86處理器中final語義的具體實現。

在編譯器中會存在如下的處理:

寫final域的重排序規則會要求編譯器在final域的寫之後,構造函數return之前插入一個StoreStore屏障

讀final域的重排序規則要求編譯器在讀final域的操作前插入一個LoadLoad屏障

但是,由於X86處理器不會對寫-寫操作做重排序,所以在X86處理器中,寫final域需要的StoreStore屏障會被省略。同樣,由於X86處理器不會對存在間接依賴關系的操作做重排序,所以在X86處理器中,讀final域需要的LoadLoad屏障也會被省略掉。因此,在X86處理器中,final域的讀/寫不會插入任何內存屏障。

7、JSR-133為什麼要增強final的語義

在舊的Java內存模型中,一個最嚴重的缺陷就是現場可能看到final域的值會改變。比如一個線程讀取一個被final域的值為0(未初始化之前的默認值),過一段時間再讀取初始化後的final域的值,卻發現變為了1。因此為了修復此漏洞,JSR-133增強了final語義。

總結:

通過為final增加寫和讀重排序規則,可以為Java程序員提供初始化安全保障:只要對象正確構造(被構造對象額引用在構造函數中沒有「逸出」),那麼不需要使用同步原語(volatile和lock的使用)就可以保障任意線程都能看到這個final域在構造函數中被初始化之後的值。

文章總結至《Java並發編程藝術》,下篇總結「happens-before」,敬請關注。

『貳』 我們一起學並發編程:Java內存模型(七)happens-before

簡介

happens-before是JMM的核心概念,理解happens-before是了解JMM的關鍵。

1、設計意圖

JMM的設計需要考慮兩個方面,分別是程序員角度和編譯器、處理器角度:

程序員角度,希望內存模型易於理解、易於編程。希望是一個強內存模型。

編譯器和處理器角度,希望減少對它們的束縛,以至於編譯器和處理器可以做更多的性能優化。希望是一個弱內存模型。

因此JSR-133專家組設計JMM的核心目標就兩個:

為程序員提供足夠強的內存模型

對編譯器和處理器的限制盡可能少

下面通過一段代碼來看JSR-133如何實現這兩個目標:

doublepi=3.14;//Adoubler=1.0;//Bdoublearea=pi*r*r//C

上述代碼存在如下happens-before關系:

Ahappens-beforeB

Bhappens-beforeC

Ahappens-beforeC

這3個happens-before關系中,第二個和第三個是必須的,而第一個是非必須的(A、B操作之間重排序,程序執行結果不會發生改變)。

JMM把happens-before要求禁止的重排序分為下面的兩類:

會改變程序執行結果的重排序

不會改變程序執行結果的重排序

JMM對這兩種不同性質的重排序,採取了不同的策略:

對於會改變程序執行結果的重排序,JMM要求編譯器和處理器必須禁止

對於不會改變程序執行結果的重排序,JMM不做要求(JMM運行)

JMM設計示意圖:

JMM設計示意圖

總結:

JMM給程序員提供的happens-before規則能滿足程序員的需求。簡單易懂,具有足夠強的內存可見性保證。

JMM對編譯器和處理器的束縛盡可能少。遵循的原則是:不改變程序的執行結果(正確同步或單線程執行),編譯器和處理器可以任意優化。

2、happens-before的定義

起源:

happens-before規則來源於LeslieLamport《Time,》。該論文中使用happens-before來定義分布式系統中事件之間的偏序關系(partialordering),該文中給出了一個分布式演算法,能用來將偏序關系擴展為某種全序關系。

Java中的應用:

JSR-133使用happens-before來指定兩個操作之間的執行順序。JMM可以通過happens-before關系向程序員提供跨線程的內存可見性保證。

《JSR-133:》對happens-before關系的定義如下:

如果操作Ahappens-before操作B,那麼A操作的執行結果將會對操作B可見,且操作A的執行順序排在操作B之前——JMM對程序員的承諾

兩個操作存在happens-before關系,並不意味著Java平台的具體實現必須按照happens-before的順序來執行。如果重排序不改變程序執行結果(與happens-before)規則一致,那麼這種重排序是不非法的(JMM允許這種重排序)。——JMM對編譯器和處理器的束縛原則

happens-before和as-if-serial語義:

從上述來看,happens-before和as-if-serial語義本質上是一回事

as-if-serial語義保證單線程內程序的執行結果不被改變,happens-before關系保證正確同步的多線程程序的執行結果不改變

as-if-serial語義給編程者一種單線程是按程序順序執行的幻境;happens-before關系給編程者一種正確同步的多線程是按照happens-before指定的順序執行的幻境。

兩者的目的都是為了在不改變程序執行結果的前提下,盡可能的提高程序的執行效率。

3、happens-before規則

《JSR-133:》定義了如下happens-before規則

程序順序規則

監視器鎖規則

volatile變數規則

傳遞性

start()規則

join()規則

3.1volatile寫-讀

volatile寫-讀建立的happens-before關系

happens-before關系示意圖

分析上圖:

1happens-before2和3happens-before4由程序順序規則產生。由於編譯器和處理器遵循as-if-serial語義,也就是說,as-if-serial語義保證了程序順序規則。因此可以把程序順序規則看成是對as-if-serial語義的「封裝」。

2happens-before3是有volatile規則產生。一個volatile變數的讀,總是能看到(任意線程)對這個volatile變數的最後寫入。

1happens-before4是由傳遞性規則產生的。這里的傳遞性是由volatile的內存屏障插入策略和volatile的編譯器重排序規則來共同保證的。

3.2start()規則

假設線程A在執行的過程中,通過執行ThreadB.start()來啟動線程B;同時,假設線程A在執行ThreadB.start()之前修改了一個共享變數,線程B在執行後會讀取這些共享變數。

start()程序對應的happens-before關系圖:

分析上圖:

1happens-before2由程序順序規則產生

2happens-before4由start規則產生

1happens-before4由傳遞性規則產生

因此線程A執行ThreadB.start()之前對共享變數所做的修改,在線程B執行後都將確保對線程B可見。

3.3join()規則

假設線程A執行的過程中,通過執行ThreadB.join()來等待線程B終止;則線程B在終止之前修改了一些共享變數,線程A從ThreadB.join()返回後會讀這些共享變數。

join()程序的happens-before關系圖:

分析上圖:

2happens-before4由join()規則產生

4happens-before5由程序順序規則產生

2happens-before5由傳遞性規則產生

因此線程A執行操作ThreadB.join()並成功返回,線程B中任意操作都將對線程A可見。

文章總結至《Java並發編程藝術》,下篇總結「雙重檢查所定與延遲初始化」,敬請關注。

閱讀全文

與java並發編程教程相關的資料

熱點內容
52單片機的運行頻率 瀏覽:369
dht11是單片機嗎 瀏覽:361
電信程序員能幹多少年 瀏覽:776
linux設置網卡地址 瀏覽:984
360瀏覽器的代理伺服器如何設置 瀏覽:821
程序員經常出錯 瀏覽:420
linux查看web服務 瀏覽:509
廣東惠普伺服器續保維修雲主機 瀏覽:203
時光相冊升級後加密照片怎麼分類 瀏覽:708
linuxgz格式 瀏覽:651
郵政快遞投放點用什麼app 瀏覽:739
android啟動圖片設置 瀏覽:655
php列印sql 瀏覽:910
壓縮彈簧國標規格型號 瀏覽:631
5p壓縮機多少錢 瀏覽:59
打亂數組順序php 瀏覽:135
壓縮收納袋價格 瀏覽:318
單片機控制電動機 瀏覽:94
linuxapache編譯安裝 瀏覽:580
加密雪紡料是什麼 瀏覽:620