摘要:誕生之處就支持多線程,所以自然有解決這些問(wèn)題的辦法,而且在編程語(yǔ)言領(lǐng)域處于領(lǐng)先地位。,線程規(guī)則這條是關(guān)于線程啟動(dòng)的。在語(yǔ)言里面,的語(yǔ)義本質(zhì)上是一種可見(jiàn)性,意味著事件對(duì)事件來(lái)說(shuō)是可見(jiàn)的,無(wú)論事件和事件是否發(fā)生在同一個(gè)線程里。
之前我們說(shuō)了:
1,可見(jiàn)性
2,原子性
3,有序性
3個(gè)并發(fā)BUG的之源,這三個(gè)也是編程領(lǐng)域的共性問(wèn)題。Java誕生之處就支持多線程,所以自然有解決這些問(wèn)題的辦法,而且在編程語(yǔ)言領(lǐng)域處于領(lǐng)先地位。理解Java解決并發(fā)問(wèn)題的方案,對(duì)于其他語(yǔ)言的解決方案也有觸類(lèi)旁通的效果。
我們已經(jīng)知道了,導(dǎo)致可見(jiàn)性的原因是緩存,導(dǎo)致有序性的問(wèn)題是編譯優(yōu)化。那解決問(wèn)題的辦法就是直接禁用 緩存和編譯優(yōu)化。但是直接不去使用這些是不行了,性能無(wú)法提升。
所以合理的方案是 按需禁用緩存和編譯優(yōu)化。如何做到“按需禁用”,只有編寫(xiě)代碼的程序員自己知道,所以程序需要給程序員按需禁用和編譯優(yōu)化的方法才行。
Java的內(nèi)存模型如果站在程序員的角度,可以理解為,Java內(nèi)存模型規(guī)范了JVM如何提供按需禁用緩存和編譯優(yōu)化的方法。具體來(lái)說(shuō),這些方法包括volatile,synchronized和final三個(gè)關(guān)鍵字段。
以及六項(xiàng) Happens-Before 規(guī)則。
volatile 關(guān)鍵字并不是 Java 語(yǔ)言特有的,C語(yǔ)言也有,它的原始意義就是禁用CPU緩存。
例如,我們聲明一個(gè)volatile變量 ,volatile int x = 0,它表達(dá)的是:告訴編譯器,對(duì)這個(gè)變量的讀寫(xiě),不能使用 CPU 緩存,必須從內(nèi)存中讀取或者寫(xiě)入。看起來(lái)語(yǔ)義很明確,實(shí)際情況比較困惑。
看下以下代碼:
class VolatileExample { int x = 0; volatile boolean v = false; public void writer() { x = 42; v = true; } public void reader() { if (v == true) { // 這里 x 會(huì)是多少呢? } } }
直覺(jué)上看,這里的X應(yīng)該是42,那實(shí)際應(yīng)該是多少呢?這個(gè)要看Java的版本,如果在低于 1.5 版本上運(yùn)行,x 可能是42,也有可能是 0;如果在 1.5 以上的版本上運(yùn)行,x 就是等于 42。
分析一下,為什么 1.5 以前的版本會(huì)出現(xiàn) x = 0 的情況呢?因?yàn)樽兞?x 可能被 CPU 緩存而導(dǎo)致可見(jiàn)性問(wèn)題。這個(gè)問(wèn)題在 1.5 版本已經(jīng)被圓滿解決了。Java 內(nèi)存模型在 1.5 版本對(duì) volatile 語(yǔ)義進(jìn)行了增強(qiáng)。怎么增強(qiáng)的呢?答案是一項(xiàng) Happens-Before 規(guī)則。
這里直接給出定義:
Happens-Before :前面一個(gè)操作的結(jié)果對(duì)后續(xù)操作是可見(jiàn)的。
再進(jìn)一步的講:Happens-Before 約束了編譯器的優(yōu)化行為,雖允許編譯器優(yōu)化,但是要求編譯器優(yōu)化后一定遵守 Happens-Before 規(guī)則。
看一看Java內(nèi)存模型定義了哪些重要的Happens-Before規(guī)則
1,程序的順序性規(guī)則
這條規(guī)則是指在一個(gè)線程中,按照程序順序,前面的操作 Happens-Before 于后續(xù)的任意操作。比如剛才那段示例代碼,按照程序的順序,第 6 行代碼 x = 42; Happens-Before 于第 7 行代碼 v = true;,這就是規(guī)則 1 的內(nèi)容,也比較符合單線程里面的思維:程序前面對(duì)某個(gè)變量的修改一定是對(duì)后續(xù)操作可見(jiàn)的。
2,volatile 變量規(guī)則
這條規(guī)則是指對(duì)一個(gè) volatile 變量的寫(xiě)操作,Happens-Before 于后續(xù)對(duì)這個(gè) volatile 變量的讀操作。
這個(gè)就有點(diǎn)費(fèi)解了,對(duì)一個(gè) volatile 變量的寫(xiě)操作相對(duì)于后續(xù)對(duì)這個(gè) volatile 變量的讀操作可見(jiàn)。
3,傳遞性
這條規(guī)則是指如果 A Happens-Before B,且 B Happens-Before C,那么 A Happens-Before C。
我們將規(guī)則 3 的傳遞性應(yīng)用到我們的例子中,可以看下面這幅圖:
class VolatileExample { int x = 0; volatile boolean v = false; public void writer() { x = 42; v = true; } public void reader() { if (v == true) { // 這里 x 會(huì)是多少呢? } } }
從圖中可以看到
1,x=42 Happens-Before 寫(xiě) v=true,這是規(guī)則1
2,讀v=true Happens-Before 讀變量X,這是規(guī)則2
結(jié)合傳遞性讀定義,即:
線程A的 x=42 Happens-Before 線程B的 讀變量X
java 1.5對(duì) volatile 的增強(qiáng)就是這個(gè),根據(jù)這個(gè)定義就保證了之前的 x=42的成立
4,管程中鎖的規(guī)則
這條規(guī)則是指對(duì)一個(gè)鎖的解鎖 Happens-Before 于后續(xù)對(duì)這個(gè)鎖的加鎖。
管程 (英語(yǔ):Moniters,也稱(chēng)為監(jiān)視器) 是一種程序結(jié)構(gòu),結(jié)構(gòu)內(nèi)的多個(gè)子程序(對(duì)象或模塊)形成的多個(gè)工作線程互斥訪問(wèn)共享資源。
管程 在 Java 中指的就是 synchronized,synchronized 是 Java 里對(duì)管程的實(shí)現(xiàn)。
管程中的鎖在 Java 里是隱式實(shí)現(xiàn)的,例如下面的代碼,在進(jìn)入同步塊之前,會(huì)自動(dòng)加鎖,而在代碼塊執(zhí)行完會(huì)自動(dòng)釋放鎖,加鎖以及釋放鎖都是編譯器幫我們實(shí)現(xiàn)的。
synchronized (this) { // 此處自動(dòng)加鎖 // x 是共享變量, 初始值 =10 if (this.x < 12) { this.x = 12; } } // 此處自動(dòng)解鎖
所以結(jié)合規(guī)則定義,可以這樣理解:假設(shè) x 的初始值是 10,線程 A 執(zhí)行完代碼塊后 x 的值會(huì)變成 12(執(zhí)行完自動(dòng)釋放鎖),線程 B 進(jìn)入代碼塊時(shí),能夠看到線程 A 對(duì) x 的寫(xiě)操作,也就是線程 B 能夠看到 x==12。這個(gè)也是符合我們直覺(jué)的,應(yīng)該不難理解。
5,線程 start() 規(guī)則
這條是關(guān)于線程啟動(dòng)的。它是指主線程 A 啟動(dòng)子線程 B 后,子線程 B 能夠看到主線程在啟動(dòng)子線程 B 前的操作。
換句話說(shuō)就是,如果線程 A 調(diào)用線程 B 的 start() 方法(即在線程 A 中啟動(dòng)線程 B),那么該 start() 操作 Happens-Before 于線程 B 中的任意操作。具體可參考下面示例代碼。
Thread B = new Thread(()->{ // 主線程調(diào)用 B.start() 之前 // 所有對(duì)共享變量的修改,此處皆可見(jiàn) // 此例中,var==77 }); // 此處對(duì)共享變量 var 修改 var = 77; // 主線程啟動(dòng)子線程 B.start();
6,線程 join() 規(guī)則
這條是關(guān)于線程等待的。它是指主線程 A 等待子線程 B 完成(主線程 A 通過(guò)調(diào)用子線程 B 的 join() 方法實(shí)現(xiàn)),當(dāng)子線程 B 完成后(主線程 A 中 join() 方法返回),主線程能夠“看到”子線程的操作。這里的“看到”,指的是子線程對(duì)共享變量的操作。
換句話說(shuō)就是,如果在線程 A 中,調(diào)用線程 B 的 join() 并成功返回,那么線程 B 中的任意操作 Happens-Before 于該 join() 操作的返回。具體可參考下面示例代碼。
Thread B = new Thread(()->{ // 此處對(duì)共享變量 var 修改 var = 66; }); // 例如此處對(duì)共享變量修改, // 則這個(gè)修改結(jié)果對(duì)線程 B 可見(jiàn) // 主線程啟動(dòng)子線程 B.start(); B.join() // 子線程所有對(duì)共享變量的修改 // 在主線程調(diào)用 B.join() 之后皆可見(jiàn) // 此例中,var==66過(guò)度優(yōu)化的 final
前面我們講 volatile 為的是禁用緩存以及編譯優(yōu)化,那 final關(guān)鍵字 就是告訴編譯器優(yōu)化得更好一點(diǎn)。
final 修飾變量時(shí),初衷是告訴編譯器:這個(gè)變量生而不變,可以盡量?jī)?yōu)化。但是Java編譯器在 1.5 以前的版本導(dǎo)致優(yōu)化錯(cuò)誤了。
構(gòu)造函數(shù)的錯(cuò)誤重排導(dǎo)致線程可能看到 final 變量的值會(huì)變化。詳細(xì)的案例可以參考:http://www.cs.umd.edu/~pugh/j...
當(dāng)然了,在 1.5 以后 Java 內(nèi)存模型對(duì) final 類(lèi)型變量的重排進(jìn)行了約束。現(xiàn)在只要我們提供正確構(gòu)造函數(shù)沒(méi)有“逸出”,就不會(huì)出問(wèn)題了。
在下面例子中,在構(gòu)造函數(shù)里面將 this 賦值給了全局變量 global.obj,這就是“逸出”,線程通過(guò) global.obj 讀取 x 是有可能讀到 0 的。因此我們一定要避免“逸出”。
final int x; // 錯(cuò)誤的構(gòu)造函數(shù) public FinalFieldExample() { x = 3; y = 4; // 此處就是講 this 逸出, global.obj = this; }總結(jié)
Java 的內(nèi)存模型是并發(fā)編程領(lǐng)域的一次重要?jiǎng)?chuàng)新,Happens-Before 的語(yǔ)義是一種因果關(guān)系。在現(xiàn)實(shí)世界里,如果 A 事件是導(dǎo)致 B 事件的起因,那么 A 事件一定是先于(Happens-Before)B 事件發(fā)生的,這個(gè)就是 Happens-Before 語(yǔ)義的現(xiàn)實(shí)理解。
在 Java 語(yǔ)言里面,Happens-Before 的語(yǔ)義本質(zhì)上是一種可見(jiàn)性,A Happens-Before B 意味著 A 事件對(duì) B 事件來(lái)說(shuō)是可見(jiàn)的,無(wú)論 A 事件和 B 事件是否發(fā)生在同一個(gè)線程里。例如 A 事件發(fā)生在線程 1 上,B 事件發(fā)生在線程 2 上,Happens-Before 規(guī)則保證線程 2 上也能看到 A 事件的發(fā)生。
Java 內(nèi)存模型主要分為兩部分,一部分面向你我這種編寫(xiě)并發(fā)程序的應(yīng)用開(kāi)發(fā)人員,另一部分是面向 JVM 的實(shí)現(xiàn)人員的,我們可以重點(diǎn)關(guān)注前者,也就是和編寫(xiě)并發(fā)程序相關(guān)的部分,這部分內(nèi)容的核心就是 Happens-Before 規(guī)則。
參考:
Java內(nèi)存模型
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/73758.html
摘要:同一時(shí)刻只有一個(gè)線程執(zhí)行這個(gè)條件非常重要,我們稱(chēng)之為互斥。那對(duì)于像轉(zhuǎn)賬這種有關(guān)聯(lián)關(guān)系的操作,我們應(yīng)該怎么去解決呢先把這個(gè)問(wèn)題代碼化。 在前面的分享中我們提到。 一個(gè)或者多個(gè)操作在 CPU 執(zhí)行的過(guò)程中不被中斷的特性,稱(chēng)為原子性 思考:在32位的機(jī)器上對(duì)long型變量進(jìn)行加減操作存在并發(fā)問(wèn)題,什么原因!? 原子性問(wèn)題如何解決 我們已經(jīng)知道原子性問(wèn)題是線程切換,而操作系統(tǒng)做線程切換是依賴 ...
摘要:因?yàn)楣芾砣藛T是了解手下的人員以及自己負(fù)責(zé)的事情的。處理器優(yōu)化和指令重排上面提到在在和主存之間增加緩存,在多線程場(chǎng)景下會(huì)存在緩存一致性問(wèn)題。有沒(méi)有發(fā)現(xiàn),緩存一致性問(wèn)題其實(shí)就是可見(jiàn)性問(wèn)題。 網(wǎng)上有很多關(guān)于Java內(nèi)存模型的文章,在《深入理解Java虛擬機(jī)》和《Java并發(fā)編程的藝術(shù)》等書(shū)中也都有關(guān)于這個(gè)知識(shí)點(diǎn)的介紹。但是,很多人讀完之后還是搞不清楚,甚至有的人說(shuō)自己更懵了。本文,就來(lái)整體的...
摘要:因?yàn)楣芾砣藛T是了解手下的人員以及自己負(fù)責(zé)的事情的。處理器優(yōu)化和指令重排上面提到在在和主存之間增加緩存,在多線程場(chǎng)景下會(huì)存在緩存一致性問(wèn)題。有沒(méi)有發(fā)現(xiàn),緩存一致性問(wèn)題其實(shí)就是可見(jiàn)性問(wèn)題。 網(wǎng)上有很多關(guān)于Java內(nèi)存模型的文章,在《深入理解Java虛擬機(jī)》和《Java并發(fā)編程的藝術(shù)》等書(shū)中也都有關(guān)于這個(gè)知識(shí)點(diǎn)的介紹。但是,很多人讀完之后還是搞不清楚,甚至有的人說(shuō)自己更懵了。本文,就來(lái)整體的...
摘要:掌握的內(nèi)存模型,你就是解決并發(fā)問(wèn)題最靚的仔編譯優(yōu)化說(shuō)的具體一些,這些方法包括和關(guān)鍵字,以及內(nèi)存模型中的規(guī)則。掌握的內(nèi)存模型,你就是解決并發(fā)問(wèn)題最靚的仔共享變量藍(lán)色的虛線箭頭代表禁用了緩存,黑色的實(shí)線箭頭代表直接從主內(nèi)存中讀寫(xiě)數(shù)據(jù)。 摘要:如果編寫(xiě)的并發(fā)程序出現(xiàn)問(wèn)題時(shí),很難通過(guò)調(diào)試來(lái)解決相應(yīng)的問(wèn)題,此時(shí),需要一行行的檢查代碼...
閱讀 1271·2021-11-15 18:14
閱讀 3127·2021-08-25 09:38
閱讀 2663·2019-08-30 10:55
閱讀 2673·2019-08-29 16:39
閱讀 1305·2019-08-29 15:07
閱讀 2446·2019-08-29 14:14
閱讀 810·2019-08-29 12:36
閱讀 909·2019-08-29 11:21