線程與鎖
點(diǎn)擊關(guān)注公眾號(hào),Java干貨及時(shí)送達(dá)??

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

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

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

