在 java 中,線程由 Thread 類表示,用戶建立線程的惟一方式是建立 Thread 類的一個實例,每個線程都和這樣的一個實例關聯。在相應的 Thread 實例上調用 start() 方法將啓動一個線程。html
若是沒有正確使用同步,線程表現出來的現象將會是使人疑惑的、違反直覺的。這個章節將描述多線程編程的語義問題,包括一系列的規則,這些規則定義了在多線程環境中線程對共享內存中值的修改是否對其餘線程當即可見。java編程語言內存模型定義了統一的內存模型用於屏蔽不一樣的硬件架構,在沒有歧義的狀況下,下面將用內存模型表示這個概念。java
這些語義沒有規定多線程的程序在 JVM 的實現上應該怎麼執行,而是限定了一系列規則,由 JVM 廠商來知足這些規則,即無論 JVM 的執行策略是什麼,表現出來的行爲必須是可被接受的。程序員
操做系統有本身的內存模型,C/C++ 這些語言直接使用的就是操做系統的內存模型,而 Java 爲了屏蔽各個系統的差別,定義了本身的統一的內存模型。面試
簡單說,Java 開發者再也不關心每一個 CPU 核心有本身的內存,而後共享主內存。而是把關注點轉移到:每一個線程都有本身的工做內存,全部線程共享主內存。編程
Java 提供了多種線程之間通訊的機制,其中最基本的就是使用同步 (synchronization),其使用監視器 (monitor) 來實現。java中的每一個對象都關聯了一個監視器,線程能夠對其進行加鎖和解鎖操做。在同一時間,只有一個線程能夠拿到對象上的監視器鎖。若是其餘線程在鎖被佔用期間試圖去獲取鎖,那麼將會被阻塞直到成功獲取到鎖。同時,監視器鎖能夠重入,也就是說若是線程 t 拿到了鎖,那麼線程 t 能夠在解鎖以前重複獲取鎖;每次解鎖操做會反轉一次加鎖產生的效果。數組
synchronized 有如下兩種使用方式:緩存
synchronized 代碼塊。synchronized(object) 在對某個對象上執行加鎖時,會嘗試在該對象的監視器上進行加鎖操做,只有成功獲取鎖以後,線程纔會繼續往下執行。線程獲取到了監視器鎖後,將繼續執行 synchronized 代碼塊中的代碼,若是代碼塊執行完成,或者拋出了異常,線程將會自動對該對象上的監視器執行解鎖操做。安全
synchronized 做用於方法,稱爲同步方法。同步方法被調用時,會自動執行加鎖操做,只有加鎖成功,方法體纔會獲得執行。若是被 synchronized 修飾的方法是實例方法,那麼這個實例的監視器會被鎖定。若是是 static 方法,線程會鎖住相應的 Class 對象的監視器。方法體執行完成或者異常退出後,會自動執行解鎖操做。bash
Java語言規範既不要求阻止死鎖的發生,也不要求檢測到死鎖的發生。若是線程要在多個對象上執行加鎖操做,那麼就應該使用傳統的方法來避免死鎖的發生,若是有必要的話,須要建立更高層次的不會產生死鎖的加鎖原語。(原文:Programs where threads hold (directly or indirectly) locks on multiple objects should use conventional techniques for deadlock avoidance, creating higher-level locking primitives that do not deadlock, if necessary.)多線程
java 還提供了其餘的一些同步機制,好比對 volatile 變量的讀寫、使用 java.util.concurrent 包中的同步工具類等。
同步這一節說了 Java 併發編程中最基礎的 synchronized 這個關鍵字,你們必定要理解 synchronize 的鎖是什麼,它的鎖是基於 Java 對象的監視器 monitor,因此任何對象均可以用來作鎖。有興趣的讀者能夠去了解相關知識,包括偏向鎖、輕量級鎖、重量級鎖等。
小知識點:對 Class 對象加鎖、對對象加鎖,它們之間不構成同步。synchronized 做用於靜態方法時是對 Class 對象加鎖,做用於實例方法時是對實例加鎖。
面試中常常會問到一個類中的兩個 synchronized static 方法之間是否構成同步?構成同步。
每一個 java 對象,都關聯了一個監視器,也關聯了一個等待集合。等待集合是一個線程集合。
當對象被建立出來時,它的等待集合是空的,對於向等待集合中添加或者移除線程的操做都是原子的,如下幾個操做能夠操縱這個等待集合:Object.wait, Object.notify, Object.notifyAll。
等待集合也可能受到線程的中斷狀態的影響,也受到線程中處理中斷的方法的影響。另外,sleep 方法和 join 方法能夠感知到線程的 wait 和 notify。
這裏歸納得比較簡略,沒看懂的讀者不要緊,繼續往下看就是了。
這節要講Java線程的相關知識,主要包括:
- Thread 中的 sleep、join、interrupt
- 繼承自 Object 的 wait、notify、notifyAll
- 還有 Java 的中斷,這個概念也很重要
等待操做由如下幾個方法引起:wait(),wait(long millisecs),wait(long millisecs, int nanosecs)。在後面兩個重載方法中,若是參數爲 0,即 wait(0)、wait(0, 0) 和 wait() 是等效的。
若是調用 wait 方法時沒有拋出 InterruptedException 異常,則表示正常返回。
前方高能,請讀者保持高度精神集中。
咱們在線程 t 中對對象 m 調用 m.wait() 方法,n 表明加鎖編號,同時尚未相匹配的解鎖操做,則下面的其中之一會發生:
若是 n 等於 0(如線程 t 沒有持有對象 m 的鎖),那麼會拋出 IllegalMonitorStateException 異常。
注意,若是沒有獲取到監視器鎖,wait 方法是會拋異常的,並且注意這個異常是IllegalMonitorStateException 異常。這是重要知識點,要考。
若是線程 t 調用的是 m.wait(millisecs) 或m.wait(millisecs, nanosecs),形參 millisecs 不能爲負數,nanosecs 取值應爲 [0, 999999],不然會拋出 IllegalArgumentException 異常。
若是線程 t 被中斷,此時中斷狀態爲 true,則 wait 方法將拋出 InterruptedException 異常,並將中斷狀態設置爲 false。
中斷,若是讀者不瞭解這個概念,
不然,下面的操做會順序發生:
注意:到這裏的時候,wait 參數是正常的,同時 t 沒有被中斷,而且線程 t 已經拿到了 m 的監視器鎖。
1.線程 t 會加入到對象 m 的等待集合中,執行 加鎖編號 n 對應的解鎖操做
這裏也很是關鍵,前面說了,wait 方法的調用必須是線程獲取到了對象的監視器鎖,而到這裏會進行解鎖操做。切記切記。。。
public Object object = new Object(); void thread1() { synchronized (object) { // 獲取監視器鎖 try { object.wait(); // 這裏會解鎖,這裏會解鎖,這裏會解鎖 // 順便提一下,只是解了object上的監視器鎖,若是這個線程還持有其餘對象的監視器鎖,這個時候是不會釋放的。 } catch (InterruptedException e) { // do somethings } } } 複製代碼
2.線程 t 不會執行任何進一步的指令,直到它從 m 的等待集合中移出(也就是等待喚醒)。在發生如下操做的時候,線程 t 會從 m 的等待集合中移出,而後在以後的某個時間點恢復,並繼續執行以後的指令。
並非說線程移出等待隊列就立刻往下執行,這個線程還須要從新獲取鎖才行,這裏也很關鍵,請日後看17.2.4中我寫的兩個簡單的例子。
在 m上執行了 notify 操做,並且線程 t 被選中從等待集合中移除。
在 m 上執行了 notifyAll 操做,那麼線程 t 會從等待集合中移除。
線程 t 發生了 interrupt 操做。
若是線程 t 是調用 wait(millisecs) 或者 wait(millisecs, nanosecs) 方法進入等待集合的,那麼過了millisecs 毫秒或者 (millisecs*1000000+nanosecs) 納秒後,線程 t 也會從等待集合中移出。
JVM 的「假喚醒」,雖然這是不鼓勵的,可是這種操做是被容許的,這樣 JVM 能實現將線程從等待集合中移出,而沒必要等待具體的移出指令。
注意,良好的 Java 編碼習慣是,只在循環中使用 wait 方法,這個循環等待某些條件來退出循環。
我的理解wait方法是這麼用的:
synchronized(m) { while(!canExit) { m.wait(10); // 等待10ms; 固然中斷也是經常使用的 canExit = something(); // 判斷是否能夠退出循環 } } // 2 個知識點: // 1. 必須先獲取到對象上的監視器鎖 // 2. wait 有可能被假喚醒 複製代碼
每一個線程在一系列 可能致使它從等待集合中移出的事件 中必須決定一個順序。這個順序沒必要要和其餘順序一致,可是線程必須表現爲它是按照那個順序發生的。
例如,線程 t 如今在 m 的等待集合中,無論是線程 t 中斷仍是 m 的 notify 方法被調用,這些操做事件確定存在一個順序。若是線程 t 的中斷先發生,那麼 t 會由於 InterruptedException 異常而從 wait 方法中返回,同時 m 的等待集合中的其餘線程(若是有的話)會收到這個通知。若是 m 的 notify 先發生,那麼 t 會正常從 wait 方法返回,且不會改變中斷狀態。
咱們考慮這個場景:
線程 1 和線程 2 此時都 wait 了,線程 3 調用了 :
synchronized (object) { thread1.interrupt(); //1 object.notify(); //2 } 複製代碼
原本我覺得上面的狀況 線程1 必定是拋出 InterruptedException,線程2 是正常返回的。
感謝評論留言的 xupeng.zhang,個人這個想法是錯誤的,徹底有可能線程1正常返回(即便其中斷狀態是true),線程2 一直 wait。
3.線程 t 執行編號爲 n 的加鎖操做
回去看 2 說了什麼,線程剛剛從等待集合中移出,而後這裏須要從新獲取監視器鎖才能繼續往下執行。
4.若是線程 t 在 2 的時候因爲中斷而從 m 的等待集合中移出,那麼它的中斷狀態會重置爲 false,同時 wait 方法會拋出 InterruptedException 異常。
這一節主要在講線程進出等待集合的各類狀況,同時,最好要知道中斷是怎麼用的,中斷的狀態重置發生於何時。
這裏的 1,2,3,4 的發生順序很是關鍵,你們能夠仔細再看看是否是徹底理解了,以後的幾個小節還會更具體地闡述這個,參考代碼請看 17.2.4 小節我寫的簡單的例子。
通知操做發生於調用 notify 和 notifyAll 方法。
咱們在線程 t 中對對象 m 調用 m.notify() 或 m.notifyAll() 方法,n 表明加鎖編號,同時對應的解鎖操做沒有執行,則下面的其中之一會發生:
若是 n 等於 0,拋出 IllegalMonitorStateException 異常,由於線程 t 尚未獲取到對象 m 上的鎖。
這一點很關鍵,只有獲取到了對象上的監視器鎖的線程才能夠正常調用 notify,前面咱們也說過,調用 wait 方法的時候也要先獲取鎖
若是 n 大於 0,並且這是一個 notify 操做,若是 m 的等待集合不爲空,那麼等待集合中的線程 u 被選中從等待集合中移出。
對於哪一個線程會被選中而被移出,虛擬機沒有提供任何保證,從等待集合中將線程 u 移出,可讓線程 u 得以恢復。注意,恢復以後的線程 u 若是對 m 進行加鎖操做將不會成功,直到線程 t 徹底釋放鎖以後。
由於線程 t 這個時候還持有 m 的鎖。這個知識點在 17.2.4 節我還會重點說。這裏記住,被 notify 的線程在喚醒後是須要從新獲取監視器鎖的。
若是 n 大於 0,並且這是一個 notifyAll 操做,那麼等待集合中的全部線程都將從等待集合中移出,而後恢復。
注意,這些線程恢復後,只有一個線程能夠鎖住監視器。
本小節結束,通知操做相對來講仍是很簡單的吧。
中斷髮生於 Thread.interrupt 方法的調用。
令線程 t 調用線程 u 上的方法 u.interrupt(),其中 t 和 u 能夠是同一個線程,這個操做會將 u 的中斷狀態設置爲 true。
順便說說中斷狀態吧,初學者確定覺得 thread.interrupt() 方法是用來暫停線程的,主要是和它對應中文翻譯的「中斷」有關。中斷在併發中是經常使用的手段,請你們必定好好掌握。能夠將中斷理解爲線程的狀態,它的特殊之處在於設置了中斷狀態爲 true 後,這幾個方法會感知到:
wait(), wait(long), wait(long, int), join(), join(long), join(long, int), sleep(long), sleep(long, int)
這些方法都有一個共同之處,方法簽名上都有
throws InterruptedException
,這個就是用來響應中斷狀態修改的。若是線程阻塞在 InterruptibleChannel 類的 IO 操做中,那麼這個 channel 會被關閉。
若是線程阻塞在一個 Selector 中,那麼 select 方法會當即返回。
若是線程阻塞在以上3種狀況中,那麼當線程感知到中斷狀態後(此線程的 interrupt() 方法被調用),會將中斷狀態從新設置爲 false,而後執行相應的操做(一般就是跳到 catch 異常處)。
若是不是以上3種狀況,那麼,線程的 interrupt() 方法被調用,會將線程的中斷狀態設置爲 true。
固然,除了這幾個方法,我知道的是 LockSupport 中的 park 方法也能自動感知到線程被中斷,固然,它不會重置中斷狀態爲 false。咱們說了,只有上面的幾種狀況會在感知到中斷後先重置中斷狀態爲 false,而後再繼續執行。
另外,若是有一個對象 m,並且線程 u 此時在 m 的等待集合中,那麼 u 將會從 m 的等待集合中移出。這會讓 u 從 wait 操做中恢復過來,u 此時須要獲取 m 的監視器鎖,獲取完鎖之後,發現線程 u 處於中斷狀態,此時會拋出 InterruptedException 異常。
這裏的流程:t 設置 u 的中斷狀態 => u 線程恢復 => u 獲取 m 的監視器鎖 => 獲取鎖之後,拋出 InterruptedException 異常。
這個流程在前面 wait 的小節已經講過了,這也是不少人都不瞭解的知識點。若是還不懂,能夠看下一小節的結束,個人兩個簡單的例子。
一個小細節:u 被中斷,wait 方法返回,並不會當即拋出 InterruptedException 異常,而是在從新獲取監視器鎖以後纔會拋出異常。
實例方法 thread.isInterrupted() 能夠知道線程的中斷狀態。
調用靜態方法 Thread.interrupted() 能夠返回當前線程的中斷狀態,同時將中斷狀態設置爲false。
因此說,若是是這個方法調用兩次,那麼第二次必定會返回 false,由於第一次會重置狀態。固然了,前提是兩次調用的中間沒有發生設置線程中斷狀態的其餘語句。
以上的一系列規範能讓咱們肯定 在等待、通知、中斷的交互中 有關的幾個屬性。
若是一個線程在等待期間,同時發生了通知和中斷,它將發生:
從 wait 方法中正常返回,同時不改變中斷狀態(也就是說,調用 Thread.interrupted 方法將會返回 true)
因爲拋出了 InterruptedException 異常而從 wait 方法中返回,中斷狀態設置爲 false
線程可能沒有重置它的中斷狀態,同時從 wait 方法中正常返回,即第一種狀況。
也就是說,線程是從 notify 被喚醒的,因爲發生了中斷,因此中斷狀態爲 true
一樣的,通知也不能因爲中斷而丟失。
這個要說的是,線程實際上是從中斷喚醒的,那麼線程醒過來,同時中斷狀態會被重置爲 false。
假設 m 的等待集合爲 線程集合 s,而且在另外一個線程中調用了 m.notify(), 那麼將發生:
考慮是否有這個場景:x 被設置了中斷狀態,notify 選中了集合中的線程 x,那麼此次 notify 將喚醒線程 x,其餘線程(咱們假設還有其餘線程在等待)不會有變化。
答案:存在這種場景。由於這種場景是知足上述條件的,並且此時 x 的中斷狀態是 true。
注意,若是一個線程同時被中斷和通知喚醒,同時這個線程經過拋出 InterruptedException 異常從 wait 中返回,那麼等待集合中的某個其餘線程必定會被通知。
下面咱們經過 3 個例子簡單分析下 wait、notify、中斷 它們的組合使用。
第一個例子展現了 wait 和 notify 操做過程當中的監視器鎖的 持有、釋放 的問題。考慮如下操做:
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 獲取到監視器鎖");
try {
object.wait();
System.out.println("線程1 恢復啦。我爲何這麼久才恢復,由於notify方法雖然早就發生了,但是我還要獲取鎖才能繼續執行。");
} 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 拿到了監視器鎖。爲何呢,由於線程1 在 wait 方法的時候會自動釋放鎖");
System.out.println("線程2 執行 notify 操做");
object.notify();
System.out.println("線程2 執行完了 notify,先休息3秒再說。");
try {
Thread.sleep(3000);
System.out.println("線程2 休息完啦。注意了,調sleep方法和wait方法不同,不會釋放監視器鎖");
} catch (InterruptedException e) {
}
System.out.println("線程2 休息夠了,結束操做");
}
}
}, "線程2").start();
}
}
output:
線程1 獲取到監視器鎖
線程2 拿到了監視器鎖。爲何呢,由於線程1 在 wait 方法的時候會自動釋放鎖
線程2 執行 notify 操做
線程2 執行完了 notify,先休息3秒再說。
線程2 休息完啦。注意了,調sleep方法和wait方法不同,不會釋放監視器鎖
線程2 休息夠了,結束操做
線程1 恢復啦。我爲何這麼久才恢復,由於notify方法雖然早就發生了,但是我還要獲取鎖才能繼續執行。
複製代碼
上面的例子展現了,wait 方法返回後,須要從新獲取監視器鎖,才能夠繼續往下執行。
同理,咱們稍微修改下以上的程序,看下中斷和 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 獲取到監視器鎖");
try {
object.wait();
System.out.println("線程1 恢復啦。我爲何這麼久才恢復,由於notify方法雖然早就發生了,但是我還要獲取鎖才能繼續執行。");
} catch (InterruptedException e) {
System.out.println("線程1 wait方法拋出了InterruptedException異常,即便是異常,我也是要獲取到監視器鎖了纔會拋出");
}
}
}
}, "線程1");
thread1.start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (object) {
System.out.println("線程2 拿到了監視器鎖。爲何呢,由於線程1 在 wait 方法的時候會自動釋放鎖");
System.out.println("線程2 設置線程1 中斷");
thread1.interrupt();
System.out.println("線程2 執行完了 中斷,先休息3秒再說。");
try {
Thread.sleep(3000);
System.out.println("線程2 休息完啦。注意了,調sleep方法和wait方法不同,不會釋放監視器鎖");
} catch (InterruptedException e) {
}
System.out.println("線程2 休息夠了,結束操做");
}
}
}, "線程2").start();
}
}
output:
線程1 獲取到監視器鎖
線程2 拿到了監視器鎖。爲何呢,由於線程1 在 wait 方法的時候會自動釋放鎖
線程2 設置線程1 中斷
線程2 執行完了 中斷,先休息3秒再說。
線程2 休息完啦。注意了,調sleep方法和wait方法不同,不會釋放監視器鎖
線程2 休息夠了,結束操做
線程1 wait方法拋出了InterruptedException異常,即便是異常,我也是要獲取到監視器鎖了纔會拋出
複製代碼
上面的這個例子也很清楚,若是線程調用 wait 方法,當此線程被中斷的時候,wait 方法會返回,而後從新獲取監視器鎖,而後拋出 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 獲取到監視器鎖");
try {
object.wait();
System.out.println("線程1 正常恢復啦。");
} 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 獲取到監視器鎖");
try {
object.wait();
System.out.println("線程2 正常恢復啦。");
} 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 拿到了監視器鎖。");
System.out.println("線程3 設置線程1中斷");
thread1.interrupt(); // 1
waitNotify.a = 1; // 這行是爲了禁止上下的兩行中斷和notify代碼重排序
System.out.println("線程3 調用notify");
object.notify(); //2
System.out.println("線程3 調用完notify後,休息一會");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
}
System.out.println("線程3 休息夠了,結束同步代碼塊");
}
}
}, "線程3").start();
}
}
// 最多見的output:
線程1 獲取到監視器鎖
線程2 獲取到監視器鎖
線程3 拿到了監視器鎖。
線程3 設置線程1中斷
線程3 調用notify
線程3 調用完notify後,休息一會
線程3 休息夠了,結束同步代碼塊
線程2 正常恢復啦。
線程1 wait方法拋出了InterruptedException異常
複製代碼
上述輸出不是絕對的,再次感謝 xupeng.zhang。
有可能發生 線程1 是正常恢復的,雖然發生了中斷,它的中斷狀態也確實是 true,可是它沒有拋出 InterruptedException,而是正常返回。此時,thread2 將得不到喚醒,一直 wait。
Thread.sleep(millisecs) 使當前正在執行的線程休眠指定的一段時間(暫時中止執行任何指令),時間取決於參數值,精度受制於系統的定時器。休眠期間,線程不會釋聽任何的監視器鎖。線程的恢復取決於定時器和處理器的可用性,即有可用的處理器來喚醒線程。
須要注意的是,Thread.sleep 和 Thread.yield 都不具備同步的語義。在 Thread.sleep 和 Thread.yield 方法調用以前,不要求虛擬機將寄存器中的緩存刷出到共享內存中,同時也不要求虛擬機在這兩個方法調用以後,從新從共享內存中讀取數據到緩存。
while (!this.done)
Thread.sleep(1000);
複製代碼
yield 是告訴操做系統的調度器:個人cpu能夠先讓給其餘線程。注意,調度器能夠不理會這個信息。
這個方法太雞肋,幾乎沒用。
內存模型這一節比較長,請耐心閱讀
內存模型描述的是程序在 JVM 的執行過程當中對數據的讀寫是不是按照程序的規則正確執行的。Java 內存模型定義了一系列規則,這些規則定義了對共享內存的寫操做對於讀操做的可見性。
簡單地說,定義內存模型,主要就是爲了規範多線程程序中修改或者訪問同一個值的時候的行爲。對於那些自己就是線程安全的問題,這裏不作討論。
內存模型描述了程序執行時的可能的表現行爲。只要執行的結果是知足 java 內存模型的全部規則,那麼虛擬機對於具體的實現能夠自由發揮。
從側面說,無論虛擬機的實現是怎麼樣的,多線程程序的執行結果都應該是可預測的。
這裏我畫了一條線,從這條線到下一條線之間是兩個重排序的例子,若是你沒接觸過,能夠看一下,若是你已經熟悉了或者在其餘地方看過了,請直接往下滑。
示例 17.4-1 不正確的同步可能致使奇怪的結果
java語言容許 compilers 和 CPU 對執行指令進行重排序,致使咱們會常常看到似是而非的現象。
這裏沒有翻譯 compiler 爲編譯器,由於它不只僅表明編譯器,後續它會表明全部會致使指令重排序的機制。
如表 17.4-A 中所示,A 和 B 是共享屬性,r1 和 r2 是局部變量。初始時,令 A == B == 0。
表17.4-A. 重排序致使奇怪的結果 - 原始代碼
Thread 1 | Thread 2 |
---|---|
1: r2 = A; |
3: r1 = B; |
2: B = 1; |
4: A = 2; |
按照咱們的直覺來講,r2 == 2 同時 r1 == 1 應該是不可能的。直觀地說,指令 1 和 3 應該是最早執行的。若是指令 1 最早執行,那麼它應該不會看到指令 4 對 A 的寫入操做。若是指令 3 最早執行,那麼它應該不會看到執行 2 對 B 的寫入操做。
若是真的表現出了 r2==2 和 r1==1,那麼咱們應該知道,指令 4 先於指令 1 執行了。
若是在執行過程出表現出這種行爲( r2==2 和r1==1),那麼咱們能夠推斷出如下指令依次執行:指令 4 => 指令 1=> 指令 2 => 指令 3。看上去,這種順序是荒謬的。
可是,Java 是容許 compilers 對指令進行重排序的,只要保證在單線程的狀況下,能保證程序是按照咱們想要的結果進行執行,即 compilers 能夠對單線程內不產生數據依賴的語句之間進行重排序。若是指令 1 和指令 2 發生了重排序,如按照表17.4-B 所示的順序進行執行,那麼咱們就很容易看到,r2==2 和 r1==1 是可能發生的。
表 17.4-B. 重排序致使奇怪的結果 - 容許的編譯器轉換
Thread 1 | Thread 2 |
---|---|
B = 1; |
r1 = B; |
r2 = A; |
A = 2; |
B = 1; => r1 = B; => A = 2; => r2 = A;
對於不少程序員來講,這個結果看上去是 broken 的,可是這段代碼是沒有正確的同步致使的:
簡單地說,以後要講的一大堆東西主要就是爲了肯定共享內存讀寫的執行順序,不正確或者說非法的代碼就是由於讀寫同一內存地址沒有使用同步(這裏不只僅只是說synchronized),從而致使執行的結果具備不肯定性。
這個是數據競爭(data race)的一個例子。當代碼包含數據競爭時,常常會發生違反咱們直覺的結果。
有幾個機制會致使表 17.4-B 中的指令重排序。java 的 JIT 編譯器實現可能會重排序代碼,或者處理器也會作重排序操做。此外,java 虛擬機實現中的內存層次結構也會使代碼像重排序同樣。在本章中,咱們將全部這些會致使代碼重排序的東西統稱爲 compiler。
因此,後續咱們不要再簡單地將 compiler 翻譯爲編譯器,不要狹隘地理解爲 Java 編譯器。而是表明了全部可能會製造重排序的機制,包括 JVM 優化、CPU 優化等。
另外一個可能產生奇怪的結果的示例如表 17.4-C,初始時 p == q 同時 p.x == 0。這個代碼也是沒有正確使用同步的;在這些寫入共享內存的寫操做中,沒有進行強制的前後排序。
Table 17.4-C
Thread 1 | Thread 2 |
---|---|
r1 = p; |
r6 = p; |
r2 = r1.x; |
r6.x = 3; |
r3 = q; |
|
r4 = r3.x; |
|
r5 = r1.x; |
一個簡單的編譯器優化操做是會複用 r2 的結果給 r5,由於它們都是讀取 r1.x,並且在單線程語義中,r2 到 r5之間沒有其餘的相關的寫入操做,這種狀況如表 17.4-D 所示。
Table 17.4-D
Thread 1 | Thread 2 |
---|---|
r1 = p; |
r6 = p; |
r2 = r1.x; |
r6.x = 3; |
r3 = q; |
|
r4 = r3.x; |
|
r5 = r2; |
如今,咱們來考慮一種狀況,在線程1第一次讀取 r1.x 和 r3.x 之間,線程 2 執行 r6=p; r6.x=3; 編譯器進行了 r5複用 r2 結果的優化操做,那麼 r2==r5==0,r4 == 3,從程序員的角度來看,p.x 的值由 0 變爲 3,而後又變爲 0。
我簡單整理了一下:
Thread 1 | Thread 2 | 結果 |
---|---|---|
r1 = p; |
||
r2 = r1.x; |
r2 == 0 | |
r6 = p; |
||
r6.x = 3; |
||
r3 = q; |
||
r4 = r3.x; |
r4 == 3 | |
r5 = r2; |
r5 == r2 == 0 |
例子結束,回到正題
Java 內存模型定義了在程序的每一步,哪些值是內存可見的。對於隔離的每一個線程來講,其操做是由咱們線程中的語義來決定的,可是線程中讀取到的值是由內存模型來控制的。當咱們提到這點時,咱們說程序遵照線程內語義
,線程內語義說的是單線程內的語義,它容許咱們基於線程內讀操做看到的值徹底預測線程的行爲。若是咱們要肯定線程 t 中的操做是不是合法的,咱們只要評估當線程 t 在單線程環境中運行時是不是合法的就能夠,該規範的其他部分也在定義這個問題。
這段話不太好理解,首先記住「線程內語義」這個概念,以後還會用到。我對這段話的理解是,在單線程中,咱們是能夠經過一行一行看代碼來預測執行結果的,只不過,代碼中使用到的讀取內存的值咱們是不能肯定的,這取決於在內存模型這個大框架下,咱們的程序會讀到的值。也許是最新的值,也許是過期的值。
此節描述除了 final 關鍵字外的java內存模型
的規範,final將在以後的17.5節介紹。
全部線程均可以訪問到的內存稱爲共享內存
或堆內存
。
全部的實例屬性,靜態屬性,還有數組的元素都存儲在堆內存中。在本章中,咱們用術語變量
來表示這些元素。
局部變量、方法參數、異常對象,它們不會在線程間共享,也不會受到內存模型定義的任何影響。
兩個線程對同一個變量同時進行讀-寫操做
或寫-寫操做
,咱們稱之爲「衝突」。
好,這一節都是廢話,愉快地進入到下一節
這一節主要是講解理論,主要就是嚴謹地定義操做。
線程間操做
是指由一個線程執行的動做,能夠被另外一個線程檢測到或直接影響到。如下是幾種可能發生的線程間操做
:
讀 (普通變量,非 volatile)。讀一個變量。
寫 (普通變量,非 volatile)。寫一個變量。
同步操做,以下:
volatile 讀。讀一個 volatile 變量
volatile 寫。寫入一個 volatile 變量
加鎖。對一個對象的監視器加鎖。
解鎖。解除對某個對象的監視器鎖。
線程的第一個和最後一個操做。
開啓線程操做,或檢測一個線程是否已經結束。
外部操做
。一個外部操做指的是可能被觀察到的在外部執行的操做,同時它的執行結果受外部環境控制。
簡單說,外部操做的外部指的是在 JVM 以外,如 native 操做。
線程分歧操做(§17.4.9)
。此操做只由處於無限循環的線程執行,在該循環中不執行任何內存操做、同步操做、或外部操做。若是一個線程執行了分歧操做,那麼其後將跟着無數的線程分歧操做。
此規範僅關心線程間操做,咱們不關心線程內部的操做(好比將兩個局部變量的值相加存到第三個局部變量中)。如前文所說,全部的線程都須要遵照線程內語義。對於線程間操做,咱們常常會簡單地稱爲操做。
咱們用元祖<t,k,v,u>來描述一個操做:
t - 執行操做的線程
k - 操做的類型。
v - 操做涉及的變量或監視器
對於加鎖操做,v 是被鎖住的監視器;對於解鎖操做,v 是被解鎖的監視器。
若是是一個讀操做( volatile 讀或非 volatile 讀),v 是讀操做對應的變量
若是是一個寫操做( volatile 寫或非 volatile 寫),v 是寫操做對應的變量
u - 惟一的標識符標識此操做
外部動做元組還包含一個附加組件,其中包含由執行操做的線程感知的外部操做的結果。 這多是關於操做的成敗的信息,以及操做中所讀的任何值。
外部操做的參數(如哪些字節寫入哪一個 socket)不是外部操做元祖的一部分。這些參數是經過線程中的其餘操做進行設置的,並能夠經過檢查線程內語義進行肯定。它們在內存模型中沒有被明確討論。
在非終結執行中,不是全部的外部操做都是可觀察的。17.4.9小節討論非終結執行和可觀察操做。
你們看完這節最懵逼的應該是
外部操做
和線程分歧操做
,我簡單解釋下。外部操做你們能夠理解爲 Java 調用了一個 native 的方法,Java 能夠獲得這個 native 方法的返回值,可是對於具體的執行其實不感知的,意味着 Java 其實不能對這種語句進行重排序,由於 Java 沒法知道方法體會執行哪些指令。
引用 stackoverflow 中的一個例子:
// method()方法中jni()是外部操做,不會和 "foo = 42;" 這條語句進行重排序。
class Externalization {
int foo = 0;
void method() {
jni(); // 外部操做
foo = 42;
}
native void jni(); /* {
assert foo == 0; //咱們假設外部操做執行的是這個。
} */
}
複製代碼
在上面這個例子中,顯然,
jni()
與foo = 42
之間不能進行重排序。再來個線程分歧操做的例子:
// 線程分歧操做阻止了重排序,因此 "foo = 42;" 這條語句不會先執行
class ThreadDivergence {
int foo = 0;
void thread1() {
while (true){} // 線程分歧操做
foo = 42;
}
void thread2() {
assert foo == 0; // 這裏永遠不會失敗
}
}
複製代碼
在每一個線程 t 執行的全部線程間動做中,t 的程序順序是反映 根據 t 的線程內語義執行這些動做的順序 的總順序。
若是全部操做的執行順序 和 代碼中的順序一致,那麼一組操做就是連續一致
的,而且,對變量 v 的每一個讀操做 r 會看到寫操做 w 寫入的值,也就是:
寫操做 w 先於 讀操做 r 完成,而且
沒有其餘的寫操做 w' 使得 w' 在 w 以後 r 以前發生。
連續一致性
對於可見性和程序執行順序是一個很是強的保證。在這種場景下,全部的單個操做(好比讀和寫)構成一個統一的執行順序,這個執行順序和代碼出現的順序是一致的,同時每一個單個操做都是原子的,且對全部線程來講當即可見。
若是程序沒有任何的數據競爭,那麼程序的全部執行操做將表現爲連續一致。
Sequential consistency and/or freedom from data races still allows errors arising from groups of operations that need to be perceived atomically and are not.
連續一致性
和/或 數據競爭的自由仍然容許錯誤從一組操做中產生。
徹底不知道這句話是什麼意思
連續一致性的核心在於每一步的操做都是原子的,同時對於全部線程都是可見的,並且不存在重排序。因此,Java 語言定義的內存模型確定不會採用這種策略,由於它直接限制了編譯器和 JVM 的各類優化措施。
注意:不少地方所說的順序一致性就是這裏的連續一致性,英文是 Sequential consistency
每一個執行都有一個同步順序。同步順序是由執行過程當中的每一個同步操做組成的順序。對於每一個線程 t,同步操做組成的同步順序是和線程 t 中的代碼順序一致的。
雖然拗口,但畢竟說的是同步,咱們都不陌生。
同步操做包括了以下同步關係:
對於監視器 m 的解鎖與全部後續操做對於 m 的加鎖同步
對 volatile 變量 v 的寫入,與全部其餘線程後續對 v 的讀同步
啓動線程的操做與線程中的第一個操做同步。
對於每一個屬性寫入默認值(0, false,null)與每一個線程對其進行的操做同步。
儘管在建立對象完成以前對對象屬性寫入默認值有點奇怪,但從概念上來講,每一個對象都是在程序啓動時用默認值初始化來建立的。
線程 T1 的最後操做與線程 T2 發現線程 T1 已經結束同步。
線程 T2 能夠經過 T1.isAlive() 或 T1.join() 方法來判斷 T1 是否已經終結。
若是線程 T1 中斷了 T2,那麼線程 T1 的中斷操做與其餘全部線程發現 T2 被中斷了同步(經過拋出 InterruptedException 異常,或者調用 Thread.interrupted 或 Thread.isInterrupted )
以上同步順序能夠理解爲對於某資源的釋放先於其餘操做對同一資源的獲取。
好,這節相對 easy,說的就是關於 A synchronizes-with B 的一系列規則。
Happens-before 是很是重要的知識,有些地方我沒有很理解,我儘可能將原文直譯過來。想要了解更深的東西,你可能還須要查詢更多的其餘資料。
兩個操做能夠用 happens-before 來肯定它們的執行順序,若是一個操做 happens-before 於另外一個操做,那麼咱們說第一個操做對於第二個操做是可見的。
注意:happens-before 強調的是可見性問題
若是咱們分別有操做 x 和操做 y,咱們寫成 hb(x, y) 來表示 x happens-before y。
若是操做 x 和操做 y 是同一個線程的兩個操做,而且在代碼上操做 x 先於操做 y 出現,那麼有 hb(x, y)
請注意,這裏不表明不能夠重排序,只要沒有數據依賴關係,重排序就是可能的。
對象構造方法的最後一行指令 happens-before 於 finalize() 方法的第一行指令。
若是操做 x 與隨後的操做 y 構成同步,那麼 hb(x, y)。
這裏說的就是上一小節的同步順序
hb(x, y) 和 hb(y, z),那麼能夠推斷出 hb(x, z)
對象的 wait 方法關聯了加鎖和解鎖的操做,它們的 happens-before 關係便是加鎖 happens-before 解鎖。
咱們應該注意到,兩個操做之間的 happens-before 的關係並不必定表示它們在 JVM 的具體實現上必須是這個順序,若是重排序後的操做結果和合法的執行結果是一致的,那麼這種實現就不是非法的。
好比說,在線程中對對象的每一個屬性寫入初始默認值並不須要先於線程的開始,只要這個事實沒有被讀到就能夠了。
咱們能夠發現,happens-before 規則主要仍是上一節 同步順序 中的規則,加上額外的幾條
更具體地說,若是兩個操做是 happens-before 的關係,可是在代碼中它們並無這種順序,那麼就沒有必要表現出 happens-before 關係。如線程 1 對變量進行寫入,線程 2 隨後對變量進行讀操做,那麼這兩個操做是沒有 happens-before 關係的。
happens-before 關係用於定義當發生數據競爭的時候。
將上面全部的規則簡化成如下列表:
對一個監視器的解鎖操做 happens-before 於後續的對這個監視器的加鎖操做。
對 volatile 屬性的寫操做先於後續對這個屬性的讀操做。
也就是一旦寫操做完成,那麼後續的讀操做必定能讀到最新的值
線程的 start() 先於任何在線程中定義的語句。
若是 A 線程中調用了 B.join(),那麼 B 線程中的操做先於 A 線程 join() 返回以後的任何語句。
由於 join() 自己就是讓其餘線程先執行完的意思。
對象的默認初始值 happens-before 於程序中對它的其餘操做。
也就是說無論咱們要對這個對象幹什麼,這個對象即便沒有建立完成,它的各個屬性也必定有初始零值。
當程序出現兩個沒有 happens-before 關係的操做對同一數據進行訪問時,咱們稱之爲程序中有數據競爭。
除了線程間操做,數據競爭不直接影響其餘操做的語義,如讀取數組的長度、檢查轉換的執行、虛擬方法的調用。
當且僅當全部連續一致的操做都沒有數據爭用時,程序就是正確同步的。
若是一個程序是正確同步的,那麼程序中的全部操做就會表現出連續一致性。
其實就是正確同步的代碼不存在數據競爭問題,這個時候程序員不須要關心重排序是否會影響咱們的代碼,咱們的代碼執行必定會表現出連續一致。
咱們說,對變量 v 的讀操做 r 能看到對 v 的寫操做 w,若是:
非正式地,若是沒有 happens-before 關係阻止讀操做 r,那麼讀操做 r 就能看到寫操做 w 的結果。
後面的部分是關於
happens-before consistency的,我也不是很理解,感興趣的讀者請自行參閱其餘資料。
A set of actions Ais happens-before consistentif for all reads rin A, whereW(r)is the write action seen by r, it is not the case that eitherhb(r, W(r))or that there exists a write win Asuch that w.v=r.vand hb(W(r), w) andhb(w, r).
In a happens-before consistent set of actions, each read sees a write that it is allowed to see by the happens-before ordering.
Example 17.4.5-1. Happens-before Consistency
For the trace in Table 17.4.5-A, initially A == B == 0
. The trace can observe r2 == 0
and r1 == 0
and still be
Table 17.4.5-A. Behavior allowed by happens-before consistency, but not sequential consistency.
Thread 1 | Thread 2 |
---|---|
B = 1; |
A = 2; |
r2 = A; |
r1 = B; |
Since there is no synchronization, each read can see either the write of the initial value or the write by the other thread. An execution order that displays this behavior is:
1: B = 1;
3: A = 2;
2: r2 = A; // sees initial write of 0
4: r1 = B; // sees initial write of 0
複製代碼
Another execution order that is happens-before consistent is:
1: r2 = A; // sees write of A = 2
3: r1 = B; // sees write of B = 1
2: B = 1;
4: A = 2;
複製代碼
In this execution, the reads see writes that occur later in the execution order. This may seem counterintuitive, but is allowed by happens-before consistency. Allowing reads to see later writes can sometimes produce unacceptable behaviors.
關於後面的幾個小節,我本身對其理解不夠,也不但願誤導你們,若是你們感興趣的話,請參閱其餘資料。
未完成
未完成
未完成
未完成
咱們常用 final,關於它最基礎的知識是:用 final 修飾的類不能夠被繼承,用 final 修飾的方法不能夠被覆寫,用 final 修飾的屬性一旦初始化之後不能夠被修改。
固然,這節說的不是這些,這裏將闡述 final 關鍵字的深層次含義。
用 final 聲明的屬性正常狀況下初始化一次後,就不會被改變。final 屬性的語義與普通屬性的語義有一些不同。尤爲是,對於 final 屬性的讀操做,compilers 能夠自由地去除沒必要要的同步。相應地,compilers 能夠將 final 屬性的值緩存在寄存器中,而不用像普通屬性同樣從內存中從新讀取。
final 屬性同時也容許程序員不須要使用同步就能夠實現線程安全的不可變對象。一個線程安全的不可變對象對於全部線程來講都是不可變的,即便傳遞這個對象的引用存在數據競爭。這能夠提供安全的保證,即便是錯誤的或者惡意的對於這個不可變對象的使用。若是須要保證對象不可變,須要正確地使用 final 屬性域。
對象只有在構造方法結束了才被認爲徹底初始化
了。若是一個對象徹底初始化之後,一個線程持有該對象的引用,那麼這個線程必定能夠看到正確初始化的 final 屬性的值。
這個隱含了,若是屬性值不是 final 的,那就不能保證必定能夠看到正確初始化的值,可能看到初始零值。
final 屬性的使用是很是簡單的:在對象的構造方法中設置 final 屬性;同時在對象初始化完成前,不要將此對象的引用寫入到其餘線程能夠訪問到的地方。若是這個條件知足,當其餘線程看到這個對象的時候,那個線程始終能夠看到正確初始化後的對象的 final 屬性。It will also see versions of any object or array referenced by those final
fields that are at least as up-to-date as the final
fields are.
這裏面說到了一個正確初始化的問題,看過《Java併發編程實戰》的可能對這個會有印象,不要在構造方法中將 this 發佈出去。
Example 17.5-1. final Fields In The Java Memory Model
這段代碼把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。咱們假定有一個線程執行 writer() 方法,另外一個線程再執行 reader() 方法。
由於 writer() 方法在對象徹底構造後將引用寫入 f,那麼 reader() 方法將必定能夠看到初始化後的 f.x : 將讀到一個 int 值 3。然而, f.y 不是 final 的,因此程序不能保證能夠看到 4,可能會獲得 0。
Example 17.5-2. final Fields For Security
final 屬性被設計成用來保障不少操做的安全性。
考慮如下代碼,線程 1 執行:
Global.s = "/tmp/usr".substring(4);
複製代碼
同時,線程 2 執行:
String myS = Global.s;
if (myS.equals("/tmp")) System.out.println(myS);
複製代碼
String 對象是不可變對象,同時 String 操做不須要使用同步。雖然 String 的實現沒有任何的數據競爭,可是其餘使用到 String 對象的代碼多是存在數據競爭的,內存模型沒有對存在數據競爭的代碼提供安全性保證。特別是,若是 String 類中的屬性不是 final 的,那麼有可能(雖然不太可能)線程 2 會看到這個 string 對象的 offset 爲初始值 0,那麼就會出現 myS.equals("/tmp")。以後的一個操做可能會看到這個 String 對象的正確的 offset 值 4,那麼會獲得 「/usr」。Java 中的許多安全特性都依賴於 String 對象的不可變性,即便是惡意代碼在數據競爭的環境中在線程之間傳遞 String 對象的引用。
你們看這段的時候,若是要看代碼,請注意,這裏說的是 JDK6 及之前的 String 類:
public final class String
implements java.io.Serializable, Comparable<String>, 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 的時候,代碼已經變爲:
public final class String
implements java.io.Serializable, Comparable<String>, 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;
複製代碼
令 o 爲一個對象,c 爲 o 的構造方法,構造方法中對 final 的屬性 f 進行寫入值。當構造方法 c 退出的時候,會在final 屬性 f 上執行一個 freeze 操做。
注意,若是一個構造方法調用了另外一個構造方法,在被調用的構造方法中設置 final 屬性,那麼對於 final 屬性的 freeze 操做發生於被調用的構造方法結束的時候。
我沒懂這邊的 freeze 操做是什麼。
對於每個執行,讀操做的行爲被其餘的兩個偏序影響,解引用鏈
我對於解引用鏈和內存鏈徹底不熟悉,因此下面這段我就不翻譯了。
Dereference Chain: If an action a is a read or write of a field or element of an object o by a thread t that did not initialize o , then there must exist some read r by thread t that sees the address of o such that r dereferences(r, a).
Memory Chain: There are several constraints on the memory chain ordering:
Given a write w , a freeze f , an action a (that is not a read of a final
field), a read r1 of the final
field frozen by f , and a read r2 such that hb(w, f) , hb(f, a) , mc(a, r1) , and dereferences(r1, r2) , then when determining which values can be seen by r2 , we consider hb(w, r2) . (This happens-before ordering does not transitively close with other happens-before orderings.)
Note that the dereferences order is reflexive, and r1 can be the same as r2.
For reads of final
fields, the only writes that are deemed to come before the read of the final
field are the ones derived through the final
field semantics.
在構造對象的線程中,對該對象的 final 屬性的讀操做,遵照正常的 happens-before 規則。若是在構造方法內,讀某個 final 屬性晚於對這個屬性的寫操做,那麼這個讀操做能夠看到這個 final 屬性已經被定義的值,不然就會看到默認值。
在許多場景下,如反序列化,系統須要在對象構造以後改變 final 屬性的值。final 屬性能夠經過反射和其餘方法來改變。惟一的具備合理語義的模式是:對象被構造出來,而後對象中的 final 屬性被更新。在這個對象的全部 final 屬性更新操做完成以前,此對象不該該對其餘線程可見,也不該該對 final 屬性進行讀操做。對於 final 屬性的 freeze 操做發生於構造方法的結束,這個時候 final 屬性已經被設值,還有經過反射或其餘方式對於 final 屬性的更新以後。
即便是這樣,依然存在幾個難點。若是一個 final 屬性在屬性聲明的時候初始化爲一個常量表達式,對於這個 final 屬性值的變化過程也許是不可見的,由於對於這個 final 屬性的使用是在編譯時用常量表達式來替換的。
另外一個問題是,該規範容許 JVM 實現對 final 屬性進行強制優化。在一個線程內,容許對於 final 屬性的讀操做與構造方法以外的對於這個 final 屬性的修改進行重排序。
Example 17.5.3-1. 對於 final 屬性的強制優化(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 上試了好多辦法,真的無法重現出來,不過併發問題就是這樣,咱們不能重現不表明不存在。StackOverflow 上有網友說在 Sparc 上運行,惋惜我沒有 Sparc 機器。
下文將說到一個比較少見的 final-field-safe context
JVM 實現能夠提供一種方式在 final 屬性安全上下文(final-field-safe context)中執行代碼塊。若是一個對象是在
在實現中,compiler 不該該將對 final 屬性的訪問移入或移出
對於
Runnable
,執行器能夠保證在一個
Runnable
中對對象 o 的不正確的訪問不會影響同一執行器內的其餘
Runnable
中的 final 帶來的安全保障。
一般,若是一個屬性是 final
的和 static
的,那麼這個屬性是不會被改變的。可是, System.in
, System.out
, 和 System.err
是 static final
的,出於遺留的歷史緣由,它們必須容許被 System.setIn
, System.setOut
, 和 System.setErr
這幾個方法改變。咱們稱這些屬性是寫保護的,用以區分普通的 final 屬性。
public final static InputStream in = null;
public final static PrintStream out = null;
public final static PrintStream err = null;
複製代碼
編譯器須要將這些屬性與 final 屬性區別對待。例如,普通 final 屬性的讀操做對於同步是「免疫的」:鎖或 volatile 讀操做中的內存屏障並不會影響到對於 final 屬性的讀操做讀到的值。由於寫保護屬性的值是能夠被改變的,因此同步事件應該對它們有影響。所以,語義規定這些屬性被當作普通屬性,不能被用戶的代碼改變,除非是 System
類中的代碼。
實現 Java 虛擬機須要考慮的一件事情是,每一個對象屬性以及數組元素之間是獨立的,更新一個屬性或元素不能影響其餘屬性或元素的讀取與更新。尤爲是,兩個線程在分別更新 byte 數組相鄰的元素時,不能互相影響與干擾,且不須要同步來保證連續一致性。
一些處理器不提供寫入單個字節的能力。 經過簡單地讀取整個字,更新相應的字節,而後將整個字寫入內存,用這種方式在這種處理器上實現字節數組更新是非法的。 這個問題有時被稱爲字分裂(word tearing),在這種不能單獨更新單個字節的處理器上,將須要尋求其餘的方法。
請注意,對於大部分處理器來講,都沒有這個問題
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 < ITERS; 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 < LENGTH; ++i)
(threads[i] = new WordTearing(i)).start();
}
}
複製代碼
這代表寫入字節時不得覆寫相鄰的字節。
在Java內存模型中,對於 non-volatile 的 long 或 double 值的寫入是經過兩個單獨的寫操做完成的:long 和 double 是 64 位的,被分爲兩個 32 位來進行寫入。那麼可能就會致使一個線程看到了某個操做的低 32 位的寫入和另外一個操做的高 32 位的寫入。
寫入或者讀取 volatile 的 long 和 double 值是原子的。
寫入和讀取對象引用必定是原子的,無論具體實現是32位仍是64位。
將一個 64 位的 long 或 double 值的寫入分爲相鄰的兩個 32 位的寫入對於 JVM 的實現來講是很方便的。爲了性能上的考慮,JVM 的實現是能夠決定採用原子寫入仍是分爲兩個部分寫入的。
若是可能的話,咱們鼓勵 JVM 的實現避開將 64 位值的寫入分拆成兩個操做。咱們也但願程序員將共享的 64 位值操做設置爲 volatile 或者使用正確的同步,這樣能夠提供更好的兼容性。
目前來看,64 位虛擬機對於 long 和 double 的寫入都是原子的,不必加 volatile 來保證原子性。