線程與鎖
點擊關(guān)注公眾號,Java干貨及時送達??

本文主要是翻譯 + 解釋?Oracle?《The Java Language Specification, Java SE 8 Edition》?的第17章?《Threads and Locks》?,原文大概30頁pdf,我加入了很多自己的理解,希望能幫大家把規(guī)范看懂,并且從中得到很多你一直想要知道但是還不知道的知識。
注意,本文在說 Java 語言規(guī)范,不是 JVM 規(guī)范,JVM 的實現(xiàn)需要滿足語言規(guī)范中定義的內(nèi)容,但是具體的實現(xiàn)細節(jié)由各 JVM 廠商自己來決定。所以,語言規(guī)范要盡可能嚴謹全面,但是也不能限制過多,不然會限制 JVM 廠商對很多細節(jié)進行性能優(yōu)化。
我能力有限,雖然已經(jīng)很用心了,但有些地方我真的不懂,我已經(jīng)在文中標記出來了。
建議分 3 部分閱讀。
將 17.1、17.2、17.3 一起閱讀,這里關(guān)于線程中的 wait、notify、中斷有很多的知識; 17.4 的內(nèi)存模型比較長,重排序和 happens-before 關(guān)系是重點; 剩下的 final、字分裂、double和long的非原子問題,這些都是相對獨立的 topic。
Chapter 17. Threads and Locks
前言
在 java 中,線程由 Thread 類表示,用戶創(chuàng)建線程的唯一方式是創(chuàng)建 Thread 類的一個實例,每一個線程都和這樣的一個實例關(guān)聯(lián)。在相應(yīng)的 Thread 實例上調(diào)用 start() 方法將啟動一個線程。
如果沒有正確使用同步,線程表現(xiàn)出來的現(xiàn)象將會是令人疑惑的、違反直覺的。這個章節(jié)將描述多線程編程的語義問題,包括一系列的規(guī)則,這些規(guī)則定義了在多線程環(huán)境中線程對共享內(nèi)存中值的修改是否對其他線程立即可見。
java編程語言內(nèi)存模型定義了統(tǒng)一的內(nèi)存模型用于屏蔽不同的硬件架構(gòu),在沒有歧義的情況下,下面將用內(nèi)存模型表示這個概念。
這些語義沒有規(guī)定多線程的程序在 JVM 的實現(xiàn)上應(yīng)該怎么執(zhí)行,而是限定了一系列規(guī)則,由 JVM 廠商來滿足這些規(guī)則,即不管 JVM 的執(zhí)行策略是什么,表現(xiàn)出來的行為必須是可被接受的。
操作系統(tǒng)有自己的內(nèi)存模型,C/C++ 這些語言直接使用的就是操作系統(tǒng)的內(nèi)存模型,而 Java 為了屏蔽各個系統(tǒng)的差異,定義了自己的統(tǒng)一的內(nèi)存模型。簡單說,Java 開發(fā)者不再關(guān)心每個 CPU 核心有自己的內(nèi)存,然后共享主內(nèi)存。而是把關(guān)注點轉(zhuǎn)移到:每個線程都有自己的工作內(nèi)存,所有線程共享主內(nèi)存。
17.1 同步(synchronization)
Java 提供了多種線程之間通信的機制,其中最基本的就是使用同步 (synchronization),其使用監(jiān)視器 (monitor) 來實現(xiàn)。java中的每個對象都關(guān)聯(lián)了一個監(jiān)視器,線程可以對其進行加鎖和解鎖操作。
在同一時間,只有一個線程可以拿到對象上的監(jiān)視器鎖。如果其他線程在鎖被占用期間試圖去獲取鎖,那么將會被阻塞直到成功獲取到鎖。同時,監(jiān)視器鎖可以重入,也就是說如果線程 t 拿到了鎖,那么線程 t 可以在解鎖之前重復(fù)獲取鎖;每次解鎖操作會反轉(zhuǎn)一次加鎖產(chǎn)生的效果。
synchronized 有以下兩種使用方式:
synchronized 代碼塊。synchronized(object)在對某個對象上執(zhí)行加鎖時,會嘗試在該對象的監(jiān)視器上進行加鎖操作,只有成功獲取鎖之后,線程才會繼續(xù)往下執(zhí)行。線程獲取到了監(jiān)視器鎖后,將繼續(xù)執(zhí)行synchronized 代碼塊中的代碼,如果代碼塊執(zhí)行完成,或者拋出了異常,線程將會自動對該對象上的監(jiān)視器執(zhí)行解鎖操作。 synchronized 作用于方法,稱為同步方法。同步方法被調(diào)用時,會自動執(zhí)行加鎖操作,只有加鎖成功,方法體才會得到執(zhí)行。如果被 synchronized 修飾的方法是實例方法,那么這個實例的監(jiān)視器會被鎖定。如果是 static 方法,線程會鎖住相應(yīng)的?Class 對象的監(jiān)視器。方法體執(zhí)行完成或者異常退出后,會自動執(zhí)行解鎖操作。
Java語言規(guī)范既不要求阻止死鎖的發(fā)生,也不要求檢測到死鎖的發(fā)生。如果線程要在多個對象上執(zhí)行加鎖操作,那么就應(yīng)該使用傳統(tǒng)的方法來避免死鎖的發(fā)生,如果有必要的話,需要創(chuàng)建更高層次的不會產(chǎn)生死鎖的加鎖原語。
java 還提供了其他的一些同步機制,比如對 volatile 變量的讀寫、使用 java.util.concurrent 包中的同步工具類等。
同步這一節(jié)說了 Java 并發(fā)編程中最基礎(chǔ)的 synchronized 這個關(guān)鍵字,大家一定要理解 synchronize 的鎖是什么,它的鎖是基于 Java 對象的監(jiān)視器 monitor,所以任何對象都可以用來做鎖。有興趣的讀者可以去了解相關(guān)知識,包括偏向鎖、輕量級鎖、重量級鎖等。
小知識點:對 Class 對象加鎖、對對象加鎖,它們之間不構(gòu)成同步。synchronized 作用于靜態(tài)方法時是對?Class 對象加鎖,作用于實例方法時是對實例加鎖。
面試中經(jīng)常會問到一個類中的兩個 synchronized static 方法之間是否構(gòu)成同步?構(gòu)成同步。
17.2 等待集合 和 喚醒(Wait Sets and Notification)
每個 java 對象,都關(guān)聯(lián)了一個監(jiān)視器,也關(guān)聯(lián)了一個等待集合。等待集合是一個線程集合。
當對象被創(chuàng)建出來時,它的等待集合是空的,對于向等待集合中添加或者移除線程的操作都是原子的,以下幾個操作可以操縱這個等待集合:Object.wait, Object.notify, Object.notifyAll。
等待集合也可能受到線程的中斷狀態(tài)的影響,也受到線程中處理中斷的方法的影響。另外,sleep 方法和 join 方法可以感知到線程的 wait 和 notify。
這里概括得比較簡略,沒看懂的讀者沒關(guān)系,繼續(xù)往下看就是了。
這節(jié)要講Java線程的相關(guān)知識,主要包括:
Thread 中的 sleep、join、interrupt 繼承自 Object 的 wait、notify、notifyAll 還有 Java 的中斷,這個概念也很重要
17.2.1 等待 (Wait)
等待操作由以下幾個方法引發(fā):wait(),wait(long millisecs),wait(long millisecs, int nanosecs)。在后面兩個重載方法中,如果參數(shù)為 0,即 wait(0)、wait(0, 0) 和 wait() 是等效的。
如果調(diào)用 wait 方法時沒有拋出 InterruptedException 異常,則表示正常返回。
前方高能,請讀者保持高度精神集中。
我們在線程 t 中對對象 m 調(diào)用 m.wait() 方法,n 代表加鎖編號,同時還沒有相匹配的解鎖操作,則下面的其中之一會發(fā)生:
如果 n 等于 0(如線程 t 沒有持有對象 m 的鎖),那么會拋出 IllegalMonitorStateException 異常。
注意,如果沒有獲取到監(jiān)視器鎖,wait 方法是會拋異常的,而且注意這個異常是IllegalMonitorStateException異常。這是重要知識點,要考。
如果線程 t 調(diào)用的是 m.wait(millisecs) 或m.wait(millisecs, nanosecs),形參millisecs 不能為負數(shù),nanosecs 取值應(yīng)為 [0, 999999],否則會拋出IllegalArgumentException 異常。 如果線程 t 被中斷,此時中斷狀態(tài)為 true,則 wait 方法將拋出 InterruptedException 異常,并將中斷狀態(tài)設(shè)置為 false。
中斷,如果讀者不了解這個概念,可以參考我在 AQS(二) 中的介紹,這是非常重要的知識。
否則,下面的操作會順序發(fā)生:
注意:到這里的時候,wait 參數(shù)是正常的,同時 t 沒有被中斷,并且線程 t 已經(jīng)拿到了 m 的監(jiān)視器鎖。
1.線程 t 會加入到對象 m 的等待集合中,執(zhí)行 加鎖編號 n 對應(yīng)的解鎖操作
這里也非常關(guān)鍵,前面說了,wait 方法的調(diào)用必須是線程獲取到了對象的監(jiān)視器鎖,而到這里會進行解鎖操作。切記切記。。。
?public?Object?object?=?new?Object();
?void?thread1()?{
?????synchronized?(object)?{?//?獲取監(jiān)視器鎖
?????????try?{
?????????????object.wait();?//?這里會解鎖,這里會解鎖,這里會解鎖
?????????????//?順便提一下,只是解了object上的監(jiān)視器鎖,如果這個線程還持有其他對象的監(jiān)視器鎖,這個時候是不會釋放的。
?????????}?catch?(InterruptedException?e)?{
?????????????//?do?somethings
?????????}
?????}
?}
2.線程 t 不會執(zhí)行任何進一步的指令,直到它從 m 的等待集合中移出(也就是等待喚醒)。在發(fā)生以下操作的時候,線程 t 會從 m 的等待集合中移出,然后在之后的某個時間點恢復(fù),并繼續(xù)執(zhí)行之后的指令。
并不是說線程移出等待隊列就馬上往下執(zhí)行,這個線程還需要重新獲取鎖才行,這里也很關(guān)鍵,請往后看17.2.4中我寫的兩個簡單的例子。
在 m上執(zhí)行了 notify 操作,而且線程 t 被選中從等待集合中移除。 在 m 上執(zhí)行了 notifyAll 操作,那么線程 t 會從等待集合中移除。 線程 t 發(fā)生了 interrupt 操作。 如果線程 t 是調(diào)用 wait(millisecs) 或者 wait(millisecs, nanosecs) 方法進入等待集合的,那么過了millisecs 毫秒或者 (millisecs*1000000+nanosecs) 納秒后,線程 t也會從等待集合中移出。 JVM 的“假喚醒”,雖然這是不鼓勵的,但是這種操作是被允許的,這樣 JVM 能實現(xiàn)將線程從等待集合中移出,而不必等待具體的移出指令。
注意,良好的 Java 編碼習(xí)慣是,只在循環(huán)中使用 wait 方法,這個循環(huán)等待某些條件來退出循環(huán)。
個人理解wait方法是這么用的:
?synchronized(m)?{
?????while(!canExit)?{
???????m.wait(10);?//?等待10ms;?當然中斷也是常用的
???????canExit?=?something();??//?判斷是否可以退出循環(huán)
?????}
?}
?// 2 個知識點:
?//?1.?必須先獲取到對象上的監(jiān)視器鎖
?//?2.?wait?有可能被假喚醒
每個線程在一系列?可能導(dǎo)致它從等待集合中移出的事件?中必須決定一個順序。這個順序不必要和其他順序一致,但是線程必須表現(xiàn)為它是按照那個順序發(fā)生的。
例如,線程 t 現(xiàn)在在 m 的等待集合中,不管是線程 t 中斷還是 m 的 notify 方法被調(diào)用,這些操作事件肯定存在一個順序。如果線程 t 的中斷先發(fā)生,那么 t 會因為 InterruptedException 異常而從 wait 方法中返回,同時 m 的等待集合中的其他線程(如果有的話)會收到這個通知。如果 m 的 notify 先發(fā)生,那么 t 會正常從 wait 方法返回,且不會改變中斷狀態(tài)。
我們考慮這個場景:
線程 1 和線程 2 此時都 wait 了,線程 3 調(diào)用了 :
synchronized?(object)?{
????thread1.interrupt();?//1
????object.notify();??//2
}
本來我以為上面的情況 線程1 一定是拋出 InterruptedException,線程2 是正常返回的。感謝評論留言的 xupeng.zhang,我的這個想法是錯誤的,完全有可能線程1正常返回(即使其中斷狀態(tài)是true),線程2 一直 wait。
3.線程 t 執(zhí)行編號為 n 的加鎖操作
回去看 2 ?說了什么,線程剛剛從等待集合中移出,然后這里需要重新獲取監(jiān)視器鎖才能繼續(xù)往下執(zhí)行。
4.如果線程 t 在 2 的時候由于中斷而從 m 的等待集合中移出,那么它的中斷狀態(tài)會重置為 false,同時 wait 方法會拋出 InterruptedException 異常。
這一節(jié)主要在講線程進出等待集合的各種情況,同時,最好要知道中斷是怎么用的,中斷的狀態(tài)重置發(fā)生于什么時候。
這里的 1,2,3,4 的發(fā)生順序非常關(guān)鍵,大家可以仔細再看看是不是完全理解了,之后的幾個小節(jié)還會更具體地闡述這個,參考代碼請看 17.2.4 小節(jié)我寫的簡單的例子。
17.2.2 通知(Notification)
通知操作發(fā)生于調(diào)用 notify 和 notifyAll 方法。
我們在線程 t 中對對象 m 調(diào)用 m.notify() 或 m.notifyAll() 方法,n 代表加鎖編號,同時對應(yīng)的解鎖操作沒有執(zhí)行,則下面的其中之一會發(fā)生:
如果 n 等于 0,拋出 IllegalMonitorStateException 異常,因為線程 t 還沒有獲取到對象 m 上的鎖。
這一點很關(guān)鍵,只有獲取到了對象上的監(jiān)視器鎖的線程才可以正常調(diào)用 notify,前面我們也說過,調(diào)用 wait 方法的時候也要先獲取鎖
如果 n 大于 0,而且這是一個 notify 操作,如果 m 的等待集合不為空,那么等待集合中的線程 u 被選中從等待集合中移出。
對于哪個線程會被選中而被移出,虛擬機沒有提供任何保證,從等待集合中將線程 u 移出,可以讓線程 u 得以恢復(fù)。注意,恢復(fù)之后的線程 u 如果對 m 進行加鎖操作將不會成功,直到線程 t 完全釋放鎖之后。
因為線程 t 這個時候還持有 m 的鎖。這個知識點在 17.2.4 節(jié)我還會重點說。這里記住,被 notify 的線程在喚醒后是需要重新獲取監(jiān)視器鎖的。
如果 n 大于 0,而且這是一個 notifyAll 操作,那么等待集合中的所有線程都將從等待集合中移出,然后恢復(fù)。
注意,這些線程恢復(fù)后,只有一個線程可以鎖住監(jiān)視器。
本小節(jié)結(jié)束,通知操作相對來說還是很簡單的吧。
17.2.3 中斷(Interruptions)
中斷發(fā)生于 Thread.interrupt 方法的調(diào)用。
令線程 t 調(diào)用線程 u 上的方法 u.interrupt(),其中 t 和 u 可以是同一個線程,這個操作會將 u 的中斷狀態(tài)設(shè)置為 true。
順便說說中斷狀態(tài)吧,初學(xué)者肯定以為 thread.interrupt() 方法是用來暫停線程的,主要是和它對應(yīng)中文翻譯的“中斷”有關(guān)。中斷在并發(fā)中是常用的手段,請大家一定好好掌握。可以將中斷理解為線程的狀態(tài),它的特殊之處在于設(shè)置了中斷狀態(tài)為 true 后,這幾個方法會感知到:
wait(), wait(long), wait(long, int), join(), join(long), join(long, int), sleep(long), sleep(long, int)這些方法都有一個共同之處,方法簽名上都有throws InterruptedException,這個就是用來響應(yīng)中斷狀態(tài)修改的。 如果線程阻塞在 InterruptibleChannel 類的 IO 操作中,那么這個 channel 會被關(guān)閉。 如果線程阻塞在一個 Selector 中,那么 select 方法會立即返回。
如果線程阻塞在以上3種情況中,那么當線程感知到中斷狀態(tài)后(此線程的 interrupt() 方法被調(diào)用),會將中斷狀態(tài)重新設(shè)置為 false,然后執(zhí)行相應(yīng)的操作(通常就是跳到 catch 異常處)。
如果不是以上3種情況,那么,線程的 interrupt() 方法被調(diào)用,會將線程的中斷狀態(tài)設(shè)置為 true。
當然,除了這幾個方法,我知道的是 LockSupport 中的 park 方法也能自動感知到線程被中斷,當然,它不會重置中斷狀態(tài)為 false。我們說了,只有上面的幾種情況會在感知到中斷后先重置中斷狀態(tài)為 false,然后再繼續(xù)執(zhí)行。
另外,如果有一個對象 m,而且線程 u 此時在 m 的等待集合中,那么 u 將會從 m 的等待集合中移出。這會讓 u 從 wait 操作中恢復(fù)過來,u 此時需要獲取 m 的監(jiān)視器鎖,獲取完鎖以后,發(fā)現(xiàn)線程 u 處于中斷狀態(tài),此時會拋出 InterruptedException 異常。
這里的流程:t 設(shè)置 u 的中斷狀態(tài) => u 線程恢復(fù) => u 獲取 m 的監(jiān)視器鎖 => 獲取鎖以后,拋出 InterruptedException 異常。
這個流程在前面 wait 的小節(jié)已經(jīng)講過了,這也是很多人都不了解的知識點。如果還不懂,可以看下一小節(jié)的結(jié)束,我的兩個簡單的例子。
一個小細節(jié):u 被中斷,wait 方法返回,并不會立即拋出 InterruptedException 異常,而是在重新獲取監(jiān)視器鎖之后才會拋出異常。
實例方法 thread.isInterrupted() 可以知道線程的中斷狀態(tài)。
調(diào)用靜態(tài)方法 Thread.interrupted() 可以返回當前線程的中斷狀態(tài),同時將中斷狀態(tài)設(shè)置為false。
所以說,如果是這個方法調(diào)用兩次,那么第二次一定會返回 false,因為第一次會重置狀態(tài)。當然了,前提是兩次調(diào)用的中間沒有發(fā)生設(shè)置線程中斷狀態(tài)的其他語句。
17.2.4 等待、通知和中斷的交互(Interactions of Waits, Notification, and Interruption)
以上的一系列規(guī)范能讓我們確定 在等待、通知、中斷的交互中 有關(guān)的幾個屬性。
如果一個線程在等待期間,同時發(fā)生了通知和中斷,它將發(fā)生:
從 wait 方法中正常返回,同時不改變中斷狀態(tài)(也就是說,調(diào)用 Thread.interrupted 方法將會返回 true) 由于拋出了 InterruptedException 異常而從 wait 方法中返回,中斷狀態(tài)設(shè)置為 false
線程可能沒有重置它的中斷狀態(tài),同時從 wait 方法中正常返回,即第一種情況。
也就是說,線程是從 notify 被喚醒的,由于發(fā)生了中斷,所以中斷狀態(tài)為 true
同樣的,通知也不能由于中斷而丟失。
這個要說的是,線程其實是從中斷喚醒的,那么線程醒過來,同時中斷狀態(tài)會被重置為 false。
假設(shè) m 的等待集合為 線程集合 s,并且在另一個線程中調(diào)用了 m.notify(), 那么將發(fā)生:
至少有集合 s 中的一個線程正常從 wait 方法返回,或者 集合 s 中的所有線程由拋出 InterruptedException 異常而返回。
考慮是否有這個場景:x 被設(shè)置了中斷狀態(tài),notify 選中了集合中的線程 x,那么這次 notify 將喚醒線程 x,其他線程(我們假設(shè)還有其他線程在等待)不會有變化。
答案:存在這種場景。因為這種場景是滿足上述條件的,而且此時 x 的中斷狀態(tài)是 true。
注意,如果一個線程同時被中斷和通知喚醒,同時這個線程通過拋出 InterruptedException 異常從 wait 中返回,那么等待集合中的某個其他線程一定會被通知。
下面我們通過 3 個例子簡單分析下 wait、notify、中斷 它們的組合使用。
第一個例子展示了 wait 和 notify 操作過程中的監(jiān)視器鎖的 持有、釋放 的問題。考慮以下操作:
public?class?WaitNotify?{
????public?static?void?main(String[]?args)?{
????????Object?object?=?new?Object();
????????new?Thread(new?Runnable()?{
????????????@Override
????????????public?void?run()?{
????????????????synchronized?(object)?{
????????????????????System.out.println("線程1?獲取到監(jiān)視器鎖");
????????????????????try?{
????????????????????????object.wait();
????????????????????????System.out.println("線程1 恢復(fù)啦。我為什么這么久才恢復(fù),因為notify方法雖然早就發(fā)生了,可是我還要獲取鎖才能繼續(xù)執(zhí)行。");
????????????????????}?catch?(InterruptedException?e)?{
????????????????????????System.out.println("線程1?wait方法拋出了InterruptedException異常");
????????????????????}
????????????????}
????????????}
????????},?"線程1").start();
????????new?Thread(new?Runnable()?{
????????????@Override
????????????public?void?run()?{
????????????????synchronized?(object)?{
????????????????????System.out.println("線程2 拿到了監(jiān)視器鎖。為什么呢,因為線程1 在 wait 方法的時候會自動釋放鎖");
????????????????????System.out.println("線程2?執(zhí)行?notify?操作");
????????????????????object.notify();
????????????????????System.out.println("線程2 執(zhí)行完了 notify,先休息3秒再說。");
????????????????????try?{
????????????????????????Thread.sleep(3000);
????????????????????????System.out.println("線程2 休息完啦。注意了,調(diào)sleep方法和wait方法不一樣,不會釋放監(jiān)視器鎖");
????????????????????}?catch?(InterruptedException?e)?{
????????????????????}
????????????????????System.out.println("線程2?休息夠了,結(jié)束操作");
????????????????}
????????????}
????????},?"線程2").start();
????}
}
output:
線程1?獲取到監(jiān)視器鎖
線程2 拿到了監(jiān)視器鎖。為什么呢,因為線程1 在?wait?方法的時候會自動釋放鎖
線程2?執(zhí)行?notify?操作
線程2 執(zhí)行完了 notify,先休息3秒再說。
線程2 休息完啦。注意了,調(diào)sleep方法和wait方法不一樣,不會釋放監(jiān)視器鎖
線程2?休息夠了,結(jié)束操作
線程1 恢復(fù)啦。我為什么這么久才恢復(fù),因為notify方法雖然早就發(fā)生了,可是我還要獲取鎖才能繼續(xù)執(zhí)行。
上面的例子展示了,wait 方法返回后,需要重新獲取監(jiān)視器鎖,才可以繼續(xù)往下執(zhí)行。
同理,我們稍微修改下以上的程序,看下中斷和 wait 之間的交互:
public?class?WaitNotify?{
????public?static?void?main(String[]?args)?{
????????Object?object?=?new?Object();
????????Thread?thread1?=?new?Thread(new?Runnable()?{
????????????@Override
????????????public?void?run()?{
????????????????synchronized?(object)?{
????????????????????System.out.println("線程1?獲取到監(jiān)視器鎖");
????????????????????try?{
????????????????????????object.wait();
????????????????????????System.out.println("線程1 恢復(fù)啦。我為什么這么久才恢復(fù),因為notify方法雖然早就發(fā)生了,可是我還要獲取鎖才能繼續(xù)執(zhí)行。");
????????????????????}?catch?(InterruptedException?e)?{
????????????????????????System.out.println("線程1?wait方法拋出了InterruptedException異常,即使是異常,我也是要獲取到監(jiān)視器鎖了才會拋出");
????????????????????}
????????????????}
????????????}
????????},?"線程1");
????????thread1.start();
????????new?Thread(new?Runnable()?{
????????????@Override
????????????public?void?run()?{
????????????????synchronized?(object)?{
????????????????????System.out.println("線程2 拿到了監(jiān)視器鎖。為什么呢,因為線程1 在 wait 方法的時候會自動釋放鎖");
????????????????????System.out.println("線程2?設(shè)置線程1?中斷");
????????????????????thread1.interrupt();
????????????????????System.out.println("線程2 執(zhí)行完了?中斷,先休息3秒再說。");
????????????????????try?{
????????????????????????Thread.sleep(3000);
????????????????????????System.out.println("線程2 休息完啦。注意了,調(diào)sleep方法和wait方法不一樣,不會釋放監(jiān)視器鎖");
????????????????????}?catch?(InterruptedException?e)?{
????????????????????}
????????????????????System.out.println("線程2?休息夠了,結(jié)束操作");
????????????????}
????????????}
????????},?"線程2").start();
????}
}
output:
線程1?獲取到監(jiān)視器鎖
線程2 拿到了監(jiān)視器鎖。為什么呢,因為線程1 在?wait?方法的時候會自動釋放鎖
線程2?設(shè)置線程1?中斷
線程2 執(zhí)行完了?中斷,先休息3秒再說。
線程2 休息完啦。注意了,調(diào)sleep方法和wait方法不一樣,不會釋放監(jiān)視器鎖
線程2?休息夠了,結(jié)束操作
線程1?wait方法拋出了InterruptedException異常,即使是異常,我也是要獲取到監(jiān)視器鎖了才會拋出
上面的這個例子也很清楚,如果線程調(diào)用 wait 方法,當此線程被中斷的時候,wait 方法會返回,然后重新獲取監(jiān)視器鎖,然后拋出InterruptedException 異常。
我們再來考慮下,之前說的 notify 和中斷:
package?com.javadoop.learning;
/**
?*?Created?by?hongjie?on?2017/7/7.
?*/
public?class?WaitNotify?{
????volatile?int?a?=?0;
????public?static?void?main(String[]?args)?{
????????Object?object?=?new?Object();
????????WaitNotify?waitNotify?=?new?WaitNotify();
????????Thread?thread1?=?new?Thread(new?Runnable()?{
????????????@Override
????????????public?void?run()?{
????????????????synchronized?(object)?{
????????????????????System.out.println("線程1?獲取到監(jiān)視器鎖");
????????????????????try?{
????????????????????????object.wait();
????????????????????????System.out.println("線程1 正常恢復(fù)啦。");
????????????????????}?catch?(InterruptedException?e)?{
????????????????????????System.out.println("線程1?wait方法拋出了InterruptedException異常");
????????????????????}
????????????????}
????????????}
????????},?"線程1");
????????thread1.start();
????????Thread?thread2?=?new?Thread(new?Runnable()?{
????????????@Override
????????????public?void?run()?{
????????????????synchronized?(object)?{
????????????????????System.out.println("線程2?獲取到監(jiān)視器鎖");
????????????????????try?{
????????????????????????object.wait();
????????????????????????System.out.println("線程2 正常恢復(fù)啦。");
????????????????????}?catch?(InterruptedException?e)?{
????????????????????????System.out.println("線程2?wait方法拋出了InterruptedException異常");
????????????????????}
????????????????}
????????????}
????????},?"線程2");
????????thread2.start();
?????????//?這里讓?thread1?和?thread2?先起來,然后再起后面的?thread3
????????try?{
????????????Thread.sleep(1000);
????????}?catch?(InterruptedException?e)?{
????????}
????????new?Thread(new?Runnable()?{
????????????@Override
????????????public?void?run()?{
????????????????synchronized?(object)?{
????????????????????System.out.println("線程3 拿到了監(jiān)視器鎖。");
????????????????????System.out.println("線程3?設(shè)置線程1中斷");
????????????????????thread1.interrupt();?//?1
????????????????????waitNotify.a?=?1;?//?這行是為了禁止上下的兩行中斷和notify代碼重排序
????????????????????System.out.println("線程3?調(diào)用notify");
????????????????????object.notify();?//2
????????????????????System.out.println("線程3?調(diào)用完notify后,休息一會");
????????????????????try?{
????????????????????????Thread.sleep(3000);
????????????????????}?catch?(InterruptedException?e)?{
????????????????????}
????????????????????System.out.println("線程3?休息夠了,結(jié)束同步代碼塊");
????????????????}
????????????}
????????},?"線程3").start();
????}
}
//?最常見的output:
線程1?獲取到監(jiān)視器鎖
線程2?獲取到監(jiān)視器鎖
線程3 拿到了監(jiān)視器鎖。
線程3?設(shè)置線程1中斷
線程3?調(diào)用notify
線程3?調(diào)用完notify后,休息一會
線程3?休息夠了,結(jié)束同步代碼塊
線程2 正常恢復(fù)啦。
線程1?wait方法拋出了InterruptedException異常
上述輸出不是絕對的,有可能發(fā)生 線程1 是正常恢復(fù)的,雖然發(fā)生了中斷,它的中斷狀態(tài)也確實是 true,但是它沒有拋出 InterruptedException,而是正常返回。此時,thread2 將得不到喚醒,一直 wait。
17.3. 休眠和禮讓(Sleep and Yield)
Thread.sleep(millisecs) 使當前正在執(zhí)行的線程休眠指定的一段時間(暫時停止執(zhí)行任何指令),時間取決于參數(shù)值,精度受制于系統(tǒng)的定時器。休眠期間,線程不會釋放任何的監(jiān)視器鎖。線程的恢復(fù)取決于定時器和處理器的可用性,即有可用的處理器來喚醒線程。
需要注意的是,Thread.sleep 和 Thread.yield 都不具有同步的語義。在 Thread.sleep 和 Thread.yield 方法調(diào)用之前,不要求虛擬機將寄存器中的緩存刷出到共享內(nèi)存中,同時也不要求虛擬機在這兩個方法調(diào)用之后,重新從共享內(nèi)存中讀取數(shù)據(jù)到緩存。
例如,我們有如下代碼塊,this.done 定義為一個 non-volatile 的屬性,初始值為 false。
while?(!this.done)
????Thread.sleep(1000);
編譯器可以只讀取一次 this.done 到緩存中,然后一直使用緩存中的值,也就是說,這個循環(huán)可能永遠不會結(jié)束,即使是有其他線程將 this.done 的值修改為 true。
yield 是告訴操作系統(tǒng)的調(diào)度器:我的cpu可以先讓給其他線程。注意,調(diào)度器可以不理會這個信息。
這個方法太雞肋,幾乎沒用。
17.4 內(nèi)存模型(Memory Model)
內(nèi)存模型這一節(jié)比較長,請耐心閱讀
內(nèi)存模型描述的是程序在 JVM 的執(zhí)行過程中對數(shù)據(jù)的讀寫是否是按照程序的規(guī)則正確執(zhí)行的。Java 內(nèi)存模型定義了一系列規(guī)則,這些規(guī)則定義了對共享內(nèi)存的寫操作對于讀操作的可見性。
簡單地說,定義內(nèi)存模型,主要就是為了規(guī)范多線程程序中修改或者訪問同一個值的時候的行為。對于那些本身就是線程安全的問題,這里不做討論。
內(nèi)存模型描述了程序執(zhí)行時的可能的表現(xiàn)行為。只要執(zhí)行的結(jié)果是滿足 java 內(nèi)存模型的所有規(guī)則,那么虛擬機對于具體的實現(xiàn)可以自由發(fā)揮。
從側(cè)面說,不管虛擬機的實現(xiàn)是怎么樣的,多線程程序的執(zhí)行結(jié)果都應(yīng)該是可預(yù)測的。
虛擬機實現(xiàn)者可以自由地執(zhí)行大量的代碼轉(zhuǎn)換,包括重排序操作和刪除一些不必要的同步。
這里我畫了一條線,從這條線到下一條線之間是兩個重排序的例子,如果你沒接觸過,可以看一下,如果你已經(jīng)熟悉了或者在其他地方看過了,請直接往下滑。
示例 17.4-1 不正確的同步可能導(dǎo)致奇怪的結(jié)果
java語言允許 compilers 和 CPU 對執(zhí)行指令進行重排序,導(dǎo)致我們會經(jīng)常看到似是而非的現(xiàn)象。
這里沒有翻譯 compiler 為編譯器,因為它不僅僅代表編譯器,后續(xù)它會代表所有會導(dǎo)致指令重排序的機制。
如表 17.4-A 中所示,A 和 B 是共享屬性,r1 和 r2 是局部變量。初始時,令 A == B == 0。
表17.4-A. 重排序?qū)е缕婀值慕Y(jié)果 - 原始代碼
按照我們的直覺來說,r2 == 2 同時 r1 == 1 應(yīng)該是不可能的。直觀地說,指令 1 和 3 應(yīng)該是最先執(zhí)行的。如果指令 1 最先執(zhí)行,那么它應(yīng)該不會看到指令 4 對 A 的寫入操作。如果指令 3 最先執(zhí)行,那么它應(yīng)該不會看到執(zhí)行 2 對 B 的寫入操作。
如果真的表現(xiàn)出了 r2==2 和 r1==1,那么我們應(yīng)該知道,指令 4 先于指令 1 執(zhí)行了。
如果在執(zhí)行過程出表現(xiàn)出這種行為( r2==2 和r1==1),那么我們可以推斷出以下指令依次執(zhí)行:指令 4 => 指令 1=> 指令 2 => 指令 3。看上去,這種順序是荒謬的。
但是,Java 是允許 compilers 對指令進行重排序的,只要保證在單線程的情況下,能保證程序是按照我們想要的結(jié)果進行執(zhí)行,即 compilers 可以對單線程內(nèi)不產(chǎn)生數(shù)據(jù)依賴的語句之間進行重排序。如果指令 1 和指令 2 發(fā)生了重排序,如按照表17.4-B 所示的順序進行執(zhí)行,那么我們就很容易看到,r2==2 和 r1==1 是可能發(fā)生的。
表 17.4-B. 重排序?qū)е缕婀值慕Y(jié)果 - 允許的編譯器轉(zhuǎn)換

B = 1; ?=> ?r1 = B; ?=> ?A = 2; ?=> ?r2 = A;
對于很多程序員來說,這個結(jié)果看上去是 broken 的,但是這段代碼是沒有正確的同步導(dǎo)致的:
其中有一個線程執(zhí)行了寫操作 另一個線程對同一個屬性執(zhí)行了讀操作 同時,讀操作和寫操作沒有使用同步來確定它們之間的執(zhí)行順序
簡單地說,之后要講的一大堆東西主要就是為了確定共享內(nèi)存讀寫的執(zhí)行順序,不正確或者說非法的代碼就是因為讀寫同一內(nèi)存地址沒有使用同步(這里不僅僅只是說synchronized),從而導(dǎo)致執(zhí)行的結(jié)果具有不確定性。
這個是?數(shù)據(jù)競爭(data race)?的一個例子。當代碼包含數(shù)據(jù)競爭時,經(jīng)常會發(fā)生違反我們直覺的結(jié)果。
有幾個機制會導(dǎo)致表 17.4-B 中的指令重排序。java 的 JIT 編譯器實現(xiàn)可能會重排序代碼,或者處理器也會做重排序操作。此外,java 虛擬機實現(xiàn)中的內(nèi)存層次結(jié)構(gòu)也會使代碼像重排序一樣。在本章中,我們將所有這些會導(dǎo)致代碼重排序的東西統(tǒng)稱為 compiler。
所以,后續(xù)我們不要再簡單地將 compiler 翻譯為編譯器,不要狹隘地理解為 Java 編譯器。而是代表了所有可能會制造重排序的機制,包括 JVM 優(yōu)化、CPU 優(yōu)化等。
另一個可能產(chǎn)生奇怪的結(jié)果的示例如表 17.4-C,初始時 p == q 同時 p.x == 0。這個代碼也是沒有正確使用同步的;在這些寫入共享內(nèi)存的寫操作中,沒有進行強制的先后排序。
Table 17.4-C
一個簡單的編譯器優(yōu)化操作是會復(fù)用 r2 的結(jié)果給 r5,因為它們都是讀取 r1.x,而且在單線程語義中,r2 到 r5之間沒有其他的相關(guān)的寫入操作,這種情況如表 17.4-D 所示。
Table 17.4-D
現(xiàn)在,我們來考慮一種情況,在線程1第一次讀取 r1.x 和 r3.x 之間,線程 2 執(zhí)行 r6=p; r6.x=3; 編譯器進行了 r5復(fù)用 r2 結(jié)果的優(yōu)化操作,那么 r2==r5==0,r4 == 3,從程序員的角度來看,p.x 的值由 0 變?yōu)?3,然后又變?yōu)?0。
我簡單整理了一下:

例子結(jié)束,回到正題
Java 內(nèi)存模型定義了在程序的每一步,哪些值是內(nèi)存可見的。對于隔離的每個線程來說,其操作是由我們線程中的語義來決定的,但是線程中讀取到的值是由內(nèi)存模型來控制的。
當我們提到這點時,我們說程序遵守線程內(nèi)語義,線程內(nèi)語義說的是單線程內(nèi)的語義,它允許我們基于線程內(nèi)讀操作看到的值完全預(yù)測線程的行為。如果我們要確定線程 t 中的操作是否是合法的,我們只要評估當線程 t 在單線程環(huán)境中運行時是否是合法的就可以,該規(guī)范的其余部分也在定義這個問題。
這段話不太好理解,首先記住“線程內(nèi)語義”這個概念,之后還會用到。我對這段話的理解是,在單線程中,我們是可以通過一行一行看代碼來預(yù)測執(zhí)行結(jié)果的,只不過,代碼中使用到的讀取內(nèi)存的值我們是不能確定的,這取決于在內(nèi)存模型這個大框架下,我們的程序會讀到的值。也許是最新的值,也許是過時的值。
此節(jié)描述除了 final 關(guān)鍵字外的java內(nèi)存模型的規(guī)范,final將在之后的17.5節(jié)介紹。
這里描述的內(nèi)存模型并不是基于 ?Java 編程語言的面向?qū)ο蟆榱撕啙嵠鹨姡覀兘?jīng)常展示沒有類或方法定義的代碼片段。大多數(shù)示例包含兩個或多個線程,其中包含局部變量,共享全局變量或?qū)ο蟮膶嵗侄蔚恼Z句。我們通常使用諸如 r1 或 r2 之類的變量名來表示方法或線程本地的變量。其他線程無法訪問此類變量。
17.4.1. 共享變量(Shared Variables)
所有線程都可以訪問到的內(nèi)存稱為共享內(nèi)存或堆內(nèi)存。
所有的實例屬性,靜態(tài)屬性,還有數(shù)組的元素都存儲在堆內(nèi)存中。在本章中,我們用術(shù)語變量來表示這些元素。
局部變量、方法參數(shù)、異常對象,它們不會在線程間共享,也不會受到內(nèi)存模型定義的任何影響。
兩個線程對同一個變量同時進行讀-寫操作或寫-寫操作,我們稱之為“沖突”。
好,這一節(jié)都是廢話,愉快地進入到下一節(jié)
17.4.2. 操作(Actions)
這一節(jié)主要是講解理論,主要就是嚴謹?shù)囟x操作。
線程間操作是指由一個線程執(zhí)行的動作,可以被另一個線程檢測到或直接影響到。以下是幾種可能發(fā)生的線程間操作:
讀 (普通變量,非 volatile)。讀一個變量。 寫 (普通變量,非 volatile)。寫一個變量。 同步操作,如下: volatile 讀。讀一個 volatile 變量 volatile 寫。寫入一個 volatile 變量 加鎖。對一個對象的監(jiān)視器加鎖。 解鎖。解除對某個對象的監(jiān)視器鎖。 線程的第一個和最后一個操作。 開啟線程操作,或檢測一個線程是否已經(jīng)結(jié)束。 外部操作。一個外部操作指的是可能被觀察到的在外部執(zhí)行的操作,同時它的執(zhí)行結(jié)果受外部環(huán)境控制。
簡單說,外部操作的外部指的是在 JVM 之外,如 native 操作。
線程分歧操作(§17.4.9)。此操作只由處于無限循環(huán)的線程執(zhí)行,在該循環(huán)中不執(zhí)行任何內(nèi)存操作、同步操作、或外部操作。如果一個線程執(zhí)行了分歧操作,那么其后將跟著無數(shù)的線程分歧操作。
分歧操作的引入是為了用來說明,線程可能會導(dǎo)致其他所有線程停頓而不能繼續(xù)執(zhí)行。
此規(guī)范僅關(guān)心線程間操作,我們不關(guān)心線程內(nèi)部的操作(比如將兩個局部變量的值相加存到第三個局部變量中)。如前文所說,所有的線程都需要遵守線程內(nèi)語義。對于線程間操作,我們經(jīng)常會簡單地稱為操作。
我們用元祖< t, k, v, u >來描述一個操作:
t - 執(zhí)行操作的線程 k - 操作的類型。 v - 操作涉及的變量或監(jiān)視器 對于加鎖操作,v 是被鎖住的監(jiān)視器;對于解鎖操作,v 是被解鎖的監(jiān)視器。 如果是一個讀操作( volatile 讀或非 volatile 讀),v 是讀操作對應(yīng)的變量 如果是一個寫操作( volatile 寫或非 volatile 寫),v 是寫操作對應(yīng)的變量 u - 唯一的標識符標識此操作
外部動作元組還包含一個附加組件,其中包含由執(zhí)行操作的線程感知的外部操作的結(jié)果。這可能是關(guān)于操作的成敗的信息,以及操作中所讀的任何值。
外部操作的參數(shù)(如哪些字節(jié)寫入哪個 socket)不是外部操作元祖的一部分。這些參數(shù)是通過線程中的其他操作進行設(shè)置的,并可以通過檢查線程內(nèi)語義進行確定。它們在內(nèi)存模型中沒有被明確討論。
在非終結(jié)執(zhí)行中,不是所有的外部操作都是可觀察的。17.4.9小節(jié)討論非終結(jié)執(zhí)行和可觀察操作。
大家看完這節(jié)最懵逼的應(yīng)該是外部操作和線程分歧操作,我簡單解釋下。
外部操作大家可以理解為 Java 調(diào)用了一個 native 的方法,Java 可以得到這個 native 方法的返回值,但是對于具體的執(zhí)行其實不感知的,意味著 Java 其實不能對這種語句進行重排序,因為 Java 無法知道方法體會執(zhí)行哪些指令。
引用 stackoverflow 中的一個例子:
//?method()方法中jni()是外部操作,不會和?"foo?=?42;"?這條語句進行重排序。
class?Externalization?{?
??int?foo?=?0;?
??void?method()?{?
????jni();?//?外部操作
????foo?=?42;?
??}?
??native?void?jni();?/*?{?
??? assert foo ==?0;?//我們假設(shè)外部操作執(zhí)行的是這個。
??}?*/?
}
在上面這個例子中,顯然,jni() 與 foo = 42 之間不能進行重排序。
再來個線程分歧操作的例子:
//?線程分歧操作阻止了重排序,所以?"foo?=?42;"?這條語句不會先執(zhí)行
class?ThreadDivergence?{?
??int?foo?=?0;?
??void?thread1()?{?
????while?(true){}?//?線程分歧操作
????foo?=?42;?
??}?
??void?thread2()?{?
????assert?foo?==?0;?//?這里永遠不會失敗
??}?
}
17.4.3. 程序和程序順序(Programs and Program Order)
在每個線程 t 執(zhí)行的所有線程間動作中,t 的程序順序是反映?根據(jù) t 的線程內(nèi)語義執(zhí)行這些動作的順序?的總順序。
如果所有操作的執(zhí)行順序 和 代碼中的順序一致,那么一組操作就是連續(xù)一致的,并且,對變量 v 的每個讀操作 r 會看到寫操作 w 寫入的值,也就是:
寫操作 w 先于 讀操作 r 完成,并且 沒有其他的寫操作 w' 使得 w' 在 w 之后 r 之前發(fā)生。
連續(xù)一致性對于可見性和程序執(zhí)行順序是一個非常強的保證。在這種場景下,所有的單個操作(比如讀和寫)構(gòu)成一個統(tǒng)一的執(zhí)行順序,這個執(zhí)行順序和代碼出現(xiàn)的順序是一致的,同時每個單個操作都是原子的,且對所有線程來說立即可見。
如果程序沒有任何的數(shù)據(jù)競爭,那么程序的所有執(zhí)行操作將表現(xiàn)為連續(xù)一致。連續(xù)一致性 和/或 數(shù)據(jù)競爭的自由仍然允許錯誤從一組操作中產(chǎn)生。
完全不知道這句話是什么意思
如果我們用連續(xù)一致性作為我們的內(nèi)存模型,那我們討論的許多關(guān)于編譯器優(yōu)化和處理器優(yōu)化就是非法的。比如在17.4-C中,一旦執(zhí)行 p.x=3,那么后續(xù)對于該位置的讀操作應(yīng)該是立即可以讀到最新值的。
連續(xù)一致性的核心在于每一步的操作都是原子的,同時對于所有線程都是可見的,而且不存在重排序。所以,Java 語言定義的內(nèi)存模型肯定不會采用這種策略,因為它直接限制了編譯器和 JVM 的各種優(yōu)化措施。
注意:很多地方所說的順序一致性就是這里的連續(xù)一致性,英文是 Sequential consistency
17.4.4. 同步順序(Synchronization Order)
每個執(zhí)行都有一個同步順序。同步順序是由執(zhí)行過程中的每個同步操作組成的順序。對于每個線程 t,同步操作組成的同步順序是和線程 t 中的代碼順序一致的。
雖然拗口,但畢竟說的是同步,我們都不陌生。同步操作包括了如下同步關(guān)系:
對于監(jiān)視器 m 的解鎖與所有后續(xù)操作對于 m 的加鎖同步 對 volatile 變量 v 的寫入,與所有其他線程后續(xù)對 v 的讀同步 啟動線程的操作與線程中的第一個操作同步。 對于每個屬性寫入默認值(0, false,null)與每個線程對其進行的操作同步。 盡管在創(chuàng)建對象完成之前對對象屬性寫入默認值有點奇怪,但從概念上來說,每個對象都是在程序啟動時用默認值初始化來創(chuàng)建的。 線程 T1 的最后操作與線程 T2 發(fā)現(xiàn)線程 T1 已經(jīng)結(jié)束同步。 線程 T2 可以通過 T1.isAlive() 或 T1.join() 方法來判斷 T1 是否已經(jīng)終結(jié)。 如果線程 T1 中斷了 T2,那么線程 T1 的中斷操作與其他所有線程發(fā)現(xiàn) T2 被中斷了同步(通過拋出 InterruptedException 異常,或者調(diào)用 Thread.interrupted 或 Thread.isInterrupted )
以上同步順序可以理解為對于某資源的釋放先于其他操作對同一資源的獲取。
好,這節(jié)相對 easy,說的就是關(guān)于 A synchronizes-with B 的一系列規(guī)則。
17.4.5. Happens-before順序(Happens-before Order)
Happens-before 是非常重要的知識,有些地方我沒有很理解,我盡量將原文直譯過來。想要了解更深的東西,你可能還需要查詢更多的其他資料。
兩個操作可以用 happens-before 來確定它們的執(zhí)行順序,如果一個操作 happens-before 于另一個操作,那么我們說第一個操作對于第二個操作是可見的。
注意:happens-before 強調(diào)的是可見性問題
如果我們分別有操作 x 和操作 y,我們寫成 hb(x, y) 來表示 x happens-before y。
如果操作 x 和操作 y 是同一個線程的兩個操作,并且在代碼上操作 x 先于操作 y 出現(xiàn),那么有 hb(x, y)。請注意,這里不代表不可以重排序,只要沒有數(shù)據(jù)依賴關(guān)系,重排序就是可能的。 對象構(gòu)造方法的最后一行指令 happens-before 于 finalize() 方法的第一行指令。 如果操作 x 與隨后的操作 y 構(gòu)成同步,那么 hb(x, y)。 hb(x, y) 和 hb(y, z),那么可以推斷出 hb(x, z)
對象的 wait 方法關(guān)聯(lián)了加鎖和解鎖的操作,它們的 happens-before 關(guān)系即是加鎖 happens-before 解鎖。
我們應(yīng)該注意到,兩個操作之間的 happens-before 的關(guān)系并不一定表示它們在 JVM 的具體實現(xiàn)上必須是這個順序,如果重排序后的操作結(jié)果和合法的執(zhí)行結(jié)果是一致的,那么這種實現(xiàn)就不是非法的。
比如說,在線程中對對象的每個屬性寫入初始默認值并不需要先于線程的開始,只要這個事實沒有被讀到就可以了。
我們可以發(fā)現(xiàn),happens-before 規(guī)則主要還是上一節(jié) 同步順序 中的規(guī)則,加上額外的幾條
更具體地說,如果兩個操作是 happens-before 的關(guān)系,但是在代碼中它們并沒有這種順序,那么就沒有必要表現(xiàn)出 happens-before 關(guān)系。如線程 1 對變量進行寫入,線程 2 隨后對變量進行讀操作,那么這兩個操作是沒有 happens-before 關(guān)系的。
happens-before 關(guān)系用于定義當發(fā)生數(shù)據(jù)競爭的時候。將上面所有的規(guī)則簡化成以下列表:
對一個監(jiān)視器的解鎖操作 happens-before 于后續(xù)的對這個監(jiān)視器的加鎖操作。 對 volatile 屬性的寫操作先于后續(xù)對這個屬性的讀操作。也就是一旦寫操作完成,那么后續(xù)的讀操作一定能讀到最新的值 線程的 start() 先于任何在線程中定義的語句。 如果 A 線程中調(diào)用了 B.join(),那么 B 線程中的操作先于 A 線程 join() 返回之后的任何語句。因為 join() 本身就是讓其他線程先執(zhí)行完的意思。 對象的默認初始值 happens-before 于程序中對它的其他操作。也就是說不管我們要對這個對象干什么,這個對象即使沒有創(chuàng)建完成,它的各個屬性也一定有初始零值。
當程序出現(xiàn)兩個沒有 happens-before 關(guān)系的操作對同一數(shù)據(jù)進行訪問時,我們稱之為程序中有數(shù)據(jù)競爭。
除了線程間操作,數(shù)據(jù)競爭不直接影響其他操作的語義,如讀取數(shù)組的長度、檢查轉(zhuǎn)換的執(zhí)行、虛擬方法的調(diào)用。
因此,數(shù)據(jù)競爭不會導(dǎo)致錯誤的行為,例如為數(shù)組返回錯誤的長度。當且僅當所有連續(xù)一致的操作都沒有數(shù)據(jù)爭用時,程序就是正確同步的。
如果一個程序是正確同步的,那么程序中的所有操作就會表現(xiàn)出連續(xù)一致性。
這是一個對于程序員來說強有力的保證,程序員不需要知道重排序的原因,就可以確定他們的代碼是否包含數(shù)據(jù)爭用。因此,他們不需要知道重排序的原因,來確定他們的代碼是否是正確同步的。一旦確定了代碼是正確同步的,程序員也就不需要擔(dān)心重排序?qū)τ诖a的影響。
其實就是正確同步的代碼不存在數(shù)據(jù)競爭問題,這個時候程序員不需要關(guān)心重排序是否會影響我們的代碼,我們的代碼執(zhí)行一定會表現(xiàn)出連續(xù)一致。
程序必須正確同步,以避免當出現(xiàn)重排序時,會出現(xiàn)一系列的奇怪的行為。正確同步的使用,不能保證程序的全部行為都是正確的。
但是,它的使用可以讓程序員以很簡單的方式就能知道可能發(fā)生的行為。正確同步的程序表現(xiàn)出來的行為更不會依賴于可能的重排序。沒有使用正確同步,非常奇怪、令人疑惑、違反直覺的任何行為都是可能的。
我們說,對變量 v 的讀操作 r 能看到對 v 的寫操作 w,如果:
讀操作 r 不是先于 w 發(fā)生(比如不是 hb(r, w) ),同時沒有寫操作 w' 穿插在 w 和 r 中間(如不存在 hb(w, w') 和 hb(w', r))。非正式地,如果沒有 happens-before 關(guān)系阻止讀操作 r,那么讀操作 r 就能看到寫操作 w 的結(jié)果。
17.5. final 屬性的語義(final Field Semantics)
我們經(jīng)常使用 final,關(guān)于它最基礎(chǔ)的知識是:用 final 修飾的類不可以被繼承,用 final 修飾的方法不可以被覆寫,用 final 修飾的屬性一旦初始化以后不可以被修改。
當然,這節(jié)說的不是這些,這里將闡述 final 關(guān)鍵字的深層次含義。
用 final 聲明的屬性正常情況下初始化一次后,就不會被改變。final 屬性的語義與普通屬性的語義有一些不一樣。尤其是,對于 final 屬性的讀操作,compilers 可以自由地去除不必要的同步。相應(yīng)地,compilers 可以將 final 屬性的值緩存在寄存器中,而不用像普通屬性一樣從內(nèi)存中重新讀取。
final 屬性同時也允許程序員不需要使用同步就可以實現(xiàn)線程安全的不可變對象。一個線程安全的不可變對象對于所有線程來說都是不可變的,即使傳遞這個對象的引用存在數(shù)據(jù)競爭。
這可以提供安全的保證,即使是錯誤的或者惡意的對于這個不可變對象的使用。如果需要保證對象不可變,需要正確地使用 final 屬性域。
對象只有在構(gòu)造方法結(jié)束了才被認為完全初始化了。如果一個對象完全初始化以后,一個線程持有該對象的引用,那么這個線程一定可以看到正確初始化的 final 屬性的值。
這個隱含了,如果屬性值不是 final 的,那就不能保證一定可以看到正確初始化的值,可能看到初始零值。
final 屬性的使用是非常簡單的:在對象的構(gòu)造方法中設(shè)置 final 屬性;同時在對象初始化完成前,不要將此對象的引用寫入到其他線程可以訪問到的地方。如果這個條件滿足,當其他線程看到這個對象的時候,那個線程始終可以看到正確初始化后的對象的 final 屬性。
這里面說到了一個正確初始化的問題,看過《Java并發(fā)編程實戰(zhàn)》的可能對這個會有印象,不要在構(gòu)造方法中將 this 發(fā)布出去。
這段代碼把final屬性和普通屬性進行對比。
class?FinalFieldExample?{?
????final?int?x;
????int?y;?
????static?FinalFieldExample?f;
????public?FinalFieldExample()?{
????????x?=?3;?
????????y?=?4;?
????}?
????static?void?writer()?{
????????f?=?new?FinalFieldExample();
????}?
????static?void?reader()?{
????????if?(f?!=?null)?{
????????????int?i?=?f.x;??//?程序一定能得到?3??
????????????int?j?=?f.y;??//?也許會看到?0
????????}?
????}?
}
這個類FinalFieldExample有一個 final 屬性 x 和一個普通屬性 y。我們假定有一個線程執(zhí)行 writer() 方法,另一個線程再執(zhí)行 reader() 方法。
因為 writer() 方法在對象完全構(gòu)造后將引用寫入 f,那么 reader() 方法將一定可以看到初始化后的 f.x : 將讀到一個 int 值 3。然而, f.y 不是 final 的,所以程序不能保證可以看到 4,可能會得到 0。
final 屬性被設(shè)計成用來保障很多操作的安全性。考慮以下代碼,線程 1 執(zhí)行:
Global.s?=?"/tmp/usr".substring(4);
同時,線程 2 執(zhí)行:
String?myS?=?Global.s;?
if?(myS.equals("/tmp"))?System.out.println(myS);
String 對象是不可變對象,同時 String 操作不需要使用同步。雖然 String 的實現(xiàn)沒有任何的數(shù)據(jù)競爭,但是其他使用到 String 對象的代碼可能是存在數(shù)據(jù)競爭的,內(nèi)存模型沒有對存在數(shù)據(jù)競爭的代碼提供安全性保證。
特別是,如果 String 類中的屬性不是 final 的,那么有可能(雖然不太可能)線程 2 會看到這個 string 對象的 offset 為初始值 0,那么就會出現(xiàn) myS.equals("/tmp")。
之后的一個操作可能會看到這個 String 對象的正確的 offset 值 4,那么會得到 “/usr”。Java 中的許多安全特性都依賴于 String 對象的不可變性,即使是惡意代碼在數(shù)據(jù)競爭的環(huán)境中在線程之間傳遞 String 對象的引用。
大家看這段的時候,如果要看代碼,請注意,這里說的是 ?JDK6 及以前的 String 類:
public?final?class?String??
????implements?java.io.Serializable,?Comparable,?CharSequence??
{??
????/**?The?value?is?used?for?character?storage.?*/??
????private?final?char?value[];??
????/**?The?offset?is?the?first?index?of?the?storage?that?is?used.?*/??
????private?final?int?offset;??
????/**?The?count?is?the?number?of?characters?in?the?String.?*/??
????private?final?int?count;??
????/**?Cache?the?hash?code?for?the?string?*/??
????private?int?hash;?//?Default?to?0??
因為到 JDK7 和 JDK8 的時候,代碼已經(jīng)變?yōu)椋?/p>
public?final?class?String
????implements?java.io.Serializable,?Comparable,?CharSequence?{
????/**?The?value?is?used?for?character?storage.?*/
????private?final?char?value[];
????/**?Cache?the?hash?code?for?the?string?*/
????private?int?hash;?//?Default?to?0
????/**?use?serialVersionUID?from?JDK?1.0.2?for?interoperability?*/
????private?static?final?long?serialVersionUID?=?-6849794470754667710L;
17.5.1. final屬性的語義(Semantics of final Fields)
令 o 為一個對象,c 為 o 的構(gòu)造方法,構(gòu)造方法中對 final 的屬性 f 進行寫入值。當構(gòu)造方法 c 退出的時候,會在final 屬性 f 上執(zhí)行一個 freeze 操作。
注意,如果一個構(gòu)造方法調(diào)用了另一個構(gòu)造方法,在被調(diào)用的構(gòu)造方法中設(shè)置 final 屬性,那么對于 final 屬性的 freeze 操作發(fā)生于被調(diào)用的構(gòu)造方法結(jié)束的時候。
對于每一個執(zhí)行,讀操作的行為被其他的兩個偏序影響,解引用鏈 dereferences() 和內(nèi)存鏈 mc(),它們被認為是執(zhí)行的一部分。這些偏序必須滿足下面的約束:
17.5.2. 在構(gòu)造期間讀 final 屬性(Reading final Fields During Construction)
在構(gòu)造對象的線程中,對該對象的 final 屬性的讀操作,遵守正常的 happens-before 規(guī)則。如果在構(gòu)造方法內(nèi),讀某個 final 屬性晚于對這個屬性的寫操作,那么這個讀操作可以看到這個 final 屬性已經(jīng)被定義的值,否則就會看到默認值。
17.5.3. final 屬性的修改(Subsequent Modification of final Fields)
在許多場景下,如反序列化,系統(tǒng)需要在對象構(gòu)造之后改變 final 屬性的值。final 屬性可以通過反射和其他方法來改變。
唯一的具有合理語義的模式是:對象被構(gòu)造出來,然后對象中的 final 屬性被更新。在這個對象的所有 final 屬性更新操作完成之前,此對象不應(yīng)該對其他線程可見,也不應(yīng)該對 final 屬性進行讀操作。
對于 final 屬性的 freeze 操作發(fā)生于構(gòu)造方法的結(jié)束,這個時候 final 屬性已經(jīng)被設(shè)值,還有通過反射或其他方式對于 final 屬性的更新之后。
即使是這樣,依然存在幾個難點。如果一個 final 屬性在屬性聲明的時候初始化為一個常量表達式,對于這個 final 屬性值的變化過程也許是不可見的,因為對于這個 final 屬性的使用是在編譯時用常量表達式來替換的。
另一個問題是,該規(guī)范允許 JVM 實現(xiàn)對 final 屬性進行強制優(yōu)化。在一個線程內(nèi),允許對于 final 屬性的讀操作與構(gòu)造方法之外的對于這個 final 屬性的修改進行重排序。
對于 final 屬性的強制優(yōu)化(Aggressive Optimization of final Fields)
class?A?{
????final?int?x;
????A()?{?
????????x?=?1;?
????}?
????int?f()?{?
????????return?d(this,this);?
????}?
????int?d(A?a1,?A?a2)?{?
????????int?i?=?a1.x;?
????????g(a1);?
????????int?j?=?a2.x;?
????????return?j?-?i;?
????}
????static?void?g(A?a)?{?
????????//?利用反射將?a.x?的值修改為?2
????????//?uses?reflection?to?change?a.x?to?2?
????}?
}
在方法 d 中,編譯器允許對 x 的讀操作和方法 g 進行重排序,這樣的話,new A().f()可能會返回 -1, 0, 或 1。
我在我的 MBP 上試了好多辦法,真的沒法重現(xiàn)出來,不過并發(fā)問題就是這樣,我們不能重現(xiàn)不代表不存在。StackOverflow 上有網(wǎng)友說在 Sparc 上運行,可惜我沒有 Sparc 機器。
下文將說到一個比較少見的 final-field-safe context
JVM 實現(xiàn)可以提供一種方式在 final 屬性安全上下文(final-field-safe context)中執(zhí)行代碼塊。如果一個對象是在 final 屬性安全上下文中構(gòu)造出來的,那么在這個 final 屬性安全上下文 中對于 final 屬性的讀操作不會和相應(yīng)的對于 final 屬性的修改進行重排序。
final 屬性安全上下文還提供了額外的保障。如果一個線程已經(jīng)看到一個不正確發(fā)布的一個對象的引用,那么此線程可以看到了 final 屬性的默認值,然后,在 final 屬性安全上下文中讀取該對象的正確發(fā)布的引用,這可以保證看到正確的 final 屬性的值。在形式上,在final 屬性安全上下文中執(zhí)行的代碼被認為是一個獨立的線程(僅用于滿足 final 屬性的語義)。
在實現(xiàn)中,compiler 不應(yīng)該將對 final 屬性的訪問移入或移出final 屬性安全上下文(盡管它可以在這個執(zhí)行上下文的周邊移動,只要這個對象沒有在這個上下文中進行構(gòu)造)。
對于 final 屬性安全上下文的使用,一個恰當?shù)牡胤绞菆?zhí)行器或者線程池。在每個獨立的 final 屬性安全上下文中執(zhí)行每一個 Runnable,執(zhí)行器可以保證在一個 Runnable 中對對象 o 的不正確的訪問不會影響同一執(zhí)行器內(nèi)的其他 Runnable 中的 final 帶來的安全保障。
17.5.4. 寫保護屬性(Write-Protected Fields)
通常,如果一個屬性是 final 的和 static 的,那么這個屬性是不會被改變的。但是, System.in, System.out, 和 System.err 是 static final 的,出于遺留的歷史原因,它們必須允許被 System.setIn, System.setOut, 和 System.setErr 這幾個方法改變。我們稱這些屬性是寫保護的,用以區(qū)分普通的 final 屬性。
??public?final?static?InputStream?in?=?null;
????public?final?static?PrintStream?out?=?null;
????public?final?static?PrintStream?err?=?null;
編譯器需要將這些屬性與 final 屬性區(qū)別對待。例如,普通 final 屬性的讀操作對于同步是“免疫的”:鎖或 volatile 讀操作中的內(nèi)存屏障并不會影響到對于 final 屬性的讀操作讀到的值。因為寫保護屬性的值是可以被改變的,所以同步事件應(yīng)該對它們有影響。因此,語義規(guī)定這些屬性被當做普通屬性,不能被用戶的代碼改變,除非是 System類中的代碼。
17.6. 字分裂(Word Tearing)
實現(xiàn) Java 虛擬機需要考慮的一件事情是,每個對象屬性以及數(shù)組元素之間是獨立的,更新一個屬性或元素不能影響其他屬性或元素的讀取與更新。尤其是,兩個線程在分別更新 byte 數(shù)組相鄰的元素時,不能互相影響與干擾,且不需要同步來保證連續(xù)一致性。
一些處理器不提供寫入單個字節(jié)的能力。通過簡單地讀取整個字,更新相應(yīng)的字節(jié),然后將整個字寫入內(nèi)存,用這種方式在這種處理器上實現(xiàn)字節(jié)數(shù)組更新是非法的。這個問題有時被稱為字分裂(word tearing),在這種不能單獨更新單個字節(jié)的處理器上,將需要尋求其他的方法。
請注意,對于大部分處理器來說,都沒有這個問題
Example 17.6-1. Detection of Word Tearing
以下程序用于測試是否存在字分裂:
public?class?WordTearing?extends?Thread?{
????static?final?int?LENGTH?=?8;
????static?final?int?ITERS?=?1000000;
????static?byte[]?counts?=?new?byte[LENGTH];
????static?Thread[]?threads?=?new?Thread[LENGTH];
????final?int?id;
????WordTearing(int?i)?{
????????id?=?i;
????}
????public?void?run()?{
????????byte?v?=?0;
????????for?(int?i?=?0;?i?????????????byte?v2?=?counts[id];
????????????if?(v?!=?v2)?{
????????????????System.err.println("Word-Tearing?found:?"?+
????????????????????????"counts["?+?id?+?"]?=?"?+?v2?+
????????????????????????",?should?be?"?+?v);
????????????????return;
????????????}
????????????v++;
????????????counts[id]?=?v;
????????}
????????System.out.println("done");
????}
????public?static?void?main(String[]?args)?{
????????for?(int?i?=?0;?i?????????????(threads[i]?=?new?WordTearing(i)).start();
????}
}
這表明寫入字節(jié)時不得覆寫相鄰的字節(jié)。
17.7. double 和 long 的非原子處理 (Non-Atomic Treatment of double and long)
在Java內(nèi)存模型中,對于 non-volatile 的 long 或 double 值的寫入是通過兩個單獨的寫操作完成的:long 和 double 是 64 位的,被分為兩個 32 位來進行寫入。那么可能就會導(dǎo)致一個線程看到了某個操作的低 32 位的寫入和另一個操作的高 32 位的寫入。
寫入或者讀取 volatile 的 long 和 double 值是原子的。
寫入和讀取對象引用一定是原子的,不管具體實現(xiàn)是32位還是64位。
將一個 64 位的 long 或 double 值的寫入分為相鄰的兩個 32 位的寫入對于 JVM 的實現(xiàn)來說是很方便的。為了性能上的考慮,JVM 的實現(xiàn)是可以決定采用原子寫入還是分為兩個部分寫入的。
如果可能的話,我們鼓勵 JVM 的實現(xiàn)避開將 64 位值的寫入分拆成兩個操作。我們也希望程序員將共享的 64 位值操作設(shè)置為 volatile 或者使用正確的同步,這樣可以提供更好的兼容性。
目前來看,64 位虛擬機對于 long 和 double 的寫入都是原子的,沒必要加 volatile 來保證原子性。
來源:https://javadoop.com/post/Threads-And-Locks-md
整理:黎杜
3.?美團一面:兩個有序的數(shù)組,如何高效合并成一個有序數(shù)組?
4.?你用什么軟件做筆記?
最近面試BAT,整理一份面試資料《Java面試BATJ通關(guān)手冊》,覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務(wù)、數(shù)據(jù)庫、數(shù)據(jù)結(jié)構(gòu)等等。
獲取方式:點“在看”,關(guān)注公眾號并回復(fù)?Java?領(lǐng)取,更多內(nèi)容陸續(xù)奉上。
文章有幫助的話,在看,轉(zhuǎn)發(fā)吧。
謝謝支持喲 (*^__^*)

