主頁 > 後端開發 > 2.5W 字詳解執行緒與鎖了,面試隨便問!!

2.5W 字詳解執行緒與鎖了,面試隨便問!!

2022-08-08 15:33:29 後端開發

在 java 并發編程中,執行緒和鎖永遠是最重要的概念,語言規范雖然是規范描述,但是其中也有非常多的知識和最佳實踐是值得學習的,相信這篇文章還是可以給很多讀者提供學習參考的,

本文主要是翻譯 + 解釋 Oracle 《The Java Language Specification, Java SE 8 Edition》 的第17章 《Threads and Locks》 ,原文大概30頁pdf,我加入了很多自己的理解,希望能幫大家把規范看懂,并且從中得到很多你一直想要知道但是還不知道的知識,

注意,本文在說 Java 語言規范,不是 JVM 規范,JVM 的實作需要滿足語言規范中定義的內容,但是具體的實作細節由各 JVM 廠商自己來決定,所以,語言規范要盡可能嚴謹全面,但是也不能限制過多,不然會限制 JVM 廠商對很多細節進行性能優化,

我能力有限,雖然已經很用心了,但有些地方我真的不懂,我已經在文中標記出來了,

建議分 3 部分閱讀,

  • 將 17.1、17.2、17.3 一起閱讀,這里關于執行緒中的 wait、notify、中斷有很多的知識;
  • 17.4 的記憶體模型比較長,重排序和 happens-before 關系是重點;
  • 剩下的 final、字分裂、double和long的非原子問題,這些都是相對獨立的 topic,

Chapter 17. Threads and Locks

前言

在 java 中,執行緒由 Thread 類表示,用戶創建執行緒的唯一方式是創建 Thread 類的一個實體,每一個執行緒都和這樣的一個實體關聯,在相應的 Thread 實體上呼叫 start() 方法將啟動一個執行緒,

如果沒有正確使用同步,執行緒表現出來的現象將會是令人疑惑的、違反直覺的,這個章節將描述多執行緒編程的語意問題,包括一系列的規則,這些規則定義了在多執行緒環境中執行緒對共享記憶體中值的修改是否對其他執行緒立即可見

java編程語言記憶體模型定義了統一的記憶體模型用于屏蔽不同的硬體架構,在沒有歧義的情況下,下面將用記憶體模型表示這個概念,

這些語意沒有規定多執行緒的程式在 JVM 的實作上應該怎么執行,而是限定了一系列規則,由 JVM 廠商來滿足這些規則,即不管 JVM 的執行策略是什么,表現出來的行為必須是可被接受的,

作業系統有自己的記憶體模型,C/C++ 這些語言直接使用的就是作業系統的記憶體模型,而 Java 為了屏蔽各個系統的差異,定義了自己的統一的記憶體模型,簡單說,Java 開發者不再關心每個 CPU 核心有自己的記憶體,然后共享主記憶體,而是把關注點轉移到:每個執行緒都有自己的作業記憶體,所有執行緒共享主記憶體,

17.1 同步(synchronization)

Java 提供了多種執行緒之間通信的機制,其中最基本的就是使用同步 (synchronization),其使用監視器 (monitor) 來實作,java中的每個物件都關聯了一個監視器,執行緒可以對其進行加鎖和解鎖操作,

在同一時間,只有一個執行緒可以拿到物件上的監視器鎖,如果其他執行緒在鎖被占用期間試圖去獲取鎖,那么將會被阻塞直到成功獲取到鎖,同時,監視器鎖可以重入,也就是說如果執行緒 t 拿到了鎖,那么執行緒 t 可以在解鎖之前重復獲取鎖;每次解鎖操作會反轉一次加鎖產生的效果,

synchronized 有以下兩種使用方式:

  • synchronized 代碼塊,synchronized(object)在對某個物件上執行加鎖時,會嘗試在該物件的監視器上進行加鎖操作,只有成功獲取鎖之后,執行緒才會繼續往下執行,執行緒獲取到了監視器鎖后,將繼續執行synchronized 代碼塊中的代碼,如果代碼塊執行完成,或者拋出了例外,執行緒將會自動對該物件上的監視器執行解鎖操作,
  • synchronized 作用于方法,稱為同步方法,同步方法被呼叫時,會自動執行加鎖操作,只有加鎖成功,方法體才會得到執行,如果被 synchronized 修飾的方法是實體方法,那么這個實體的監視器會被鎖定,如果是 static 方法,執行緒會鎖住相應的 Class 物件的監視器,方法體執行完成或者例外退出后,會自動執行解鎖操作,

Java語言規范既不要求阻止死鎖的發生,也不要求檢測到死鎖的發生,如果執行緒要在多個物件上執行加鎖操作,那么就應該使用傳統的方法來避免死鎖的發生,如果有必要的話,需要創建更高層次的不會產生死鎖的加鎖原語,

java 還提供了其他的一些同步機制,比如對 volatile 變數的讀寫、使用 java.util.concurrent 包中的同步工具類等,

同步這一節說了 Java 并發編程中最基礎的 synchronized 這個關鍵字,大家一定要理解 synchronize 的鎖是什么,它的鎖是基于 Java 物件的監視器 monitor,所以任何物件都可以用來做鎖,有興趣的讀者可以去了解相關知識,包括偏向鎖、輕量級鎖、重量級鎖等,

小知識點:對 Class 物件加鎖、對物件加鎖,它們之間不構成同步,synchronized 作用于靜態方法時是對 Class 物件加鎖,作用于實體方法時是對實體加鎖,

面試中經常會問到一個類中的兩個 synchronized static 方法之間是否構成同步?構成同步,

17.2 等待集合 和 喚醒(Wait Sets and Notification)

每個 java 物件,都關聯了一個監視器,也關聯了一個等待集合,等待集合是一個執行緒集合,

當物件被創建出來時,它的等待集合是空的,對于向等待集合中添加或者移除執行緒的操作都是原子的,以下幾個操作可以操縱這個等待集合:Object.wait, Object.notify, Object.notifyAll,

等待集合也可能受到執行緒的中斷狀態的影響,也受到執行緒中處理中斷的方法的影響,另外,sleep 方法和 join 方法可以感知到執行緒的 wait 和 notify,

這里概括得比較簡略,沒看懂的讀者沒關系,繼續往下看就是了,

這節要講Java執行緒的相關知識,主要包括:

  • Thread 中的 sleep、join、interrupt
  • 繼承自 Object 的 wait、notify、notifyAll
  • 還有 Java 的中斷,這個概念也很重要

17.2.1 等待 (Wait)

等待操作由以下幾個方法引發: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,

中斷,如果讀者不了解這個概念,可以參考我在 AQS(二) 中的介紹,這是非常重要的知識,

  • 否則,下面的操作會順序發生:

注意:到這里的時候,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 小節我寫的簡單的例子,

17.2.2 通知(Notification)

通知操作發生于呼叫 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 操作,那么等待集合中的所有執行緒都將從等待集合中移出,然后恢復,

注意,這些執行緒恢復后,只有一個執行緒可以鎖住監視器,

本小節結束,通知操作相對來說還是很簡單的吧,

17.2.3 中斷(Interruptions)

中斷發生于 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,因為第一次會重置狀態,當然了,前提是兩次呼叫的中間沒有發生設定執行緒中斷狀態的其他陳述句,

17.2.4 等待、通知和中斷的互動(Interactions of Waits, Notification, and Interruption)

以上的一系列規范能讓我們確定 在等待、通知、中斷的互動中 有關的幾個屬性,

如果一個執行緒在等待期間,同時發生了通知和中斷,它將發生:

  • 從 wait 方法中正常回傳,同時不改變中斷狀態(也就是說,呼叫 Thread.interrupted 方法將會回傳 true)
  • 由于拋出了 InterruptedException 例外而從 wait 方法中回傳,中斷狀態設定為 false

執行緒可能沒有重置它的中斷狀態,同時從 wait 方法中正常回傳,即第一種情況,

也就是說,執行緒是從 notify 被喚醒的,由于發生了中斷,所以中斷狀態為 true

同樣的,通知也不能由于中斷而丟失,

這個要說的是,執行緒其實是從中斷喚醒的,那么執行緒醒過來,同時中斷狀態會被重置為 false,

假設 m 的等待集合為 執行緒集合 s,并且在另一個執行緒中呼叫了 m.notify(), 那么將發生:

  • 至少有集合 s 中的一個執行緒正常從 wait 方法回傳,或者
  • 集合 s 中的所有執行緒由拋出 InterruptedException 例外而回傳,

考慮是否有這個場景: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例外

上述輸出不是絕對的,有可能發生 執行緒1 是正常恢復的,雖然發生了中斷,它的中斷狀態也確實是 true,但是它沒有拋出 InterruptedException,而是正常回傳,此時,thread2 將得不到喚醒,一直 wait,

17.3. 休眠和禮讓(Sleep and Yield)

Thread.sleep(millisecs) 使當前正在執行的執行緒休眠指定的一段時間(暫時停止執行任何指令),時間取決于引數值,精度受制于系統的定時器,休眠期間,執行緒不會釋放任何的監視器鎖,執行緒的恢復取決于定時器和處理器的可用性,即有可用的處理器來喚醒執行緒,

需要注意的是,Thread.sleep 和 Thread.yield 都不具有同步的語意,在 Thread.sleep 和 Thread.yield 方法呼叫之前,不要求虛擬機將暫存器中的快取刷出到共享記憶體中,同時也不要求虛擬機在這兩個方法呼叫之后,重新從共享記憶體中讀取資料到快取,

例如,我們有如下代碼塊,this.done 定義為一個 non-volatile 的屬性,初始值為 false,

while (!this.done)
    Thread.sleep(1000);

編譯器可以只讀取一次 this.done 到快取中,然后一直使用快取中的值,也就是說,這個回圈可能永遠不會結束,即使是有其他執行緒將 this.done 的值修改為 true,

yield 是告訴作業系統的調度器:我的cpu可以先讓給其他執行緒,注意,調度器可以不理會這個資訊,

這個方法太雞肋,幾乎沒用,

17.4 記憶體模型(Memory Model)

記憶體模型這一節比較長,請耐心閱讀

記憶體模型描述的是程式在 JVM 的執行程序中對資料的讀寫是否是按照程式的規則正確執行的,Java 記憶體模型定義了一系列規則,這些規則定義了對共享記憶體的寫操作對于讀操作的可見性,

簡單地說,定義記憶體模型,主要就是為了規范多執行緒程式中修改或者訪問同一個值的時候的行為,對于那些本身就是執行緒安全的問題,這里不做討論,

記憶體模型描述了程式執行時的可能的表現行為,只要執行的結果是滿足 java 記憶體模型的所有規則,那么虛擬機對于具體的實作可以自由發揮,

從側面說,不管虛擬機的實作是怎么樣的,多執行緒程式的執行結果都應該是可預測的,

虛擬機實作者可以自由地執行大量的代碼轉換,包括重排序操作和洗掉一些不必要的同步,

這里我畫了一條線,從這條線到下一條線之間是兩個重排序的例子,如果你沒接觸過,可以看一下,如果你已經熟悉了或者在其他地方看過了,請直接往下滑,

示例 17.4-1 不正確的同步可能導致奇怪的結果

java語言允許 compilers 和 CPU 對執行指令進行重排序,導致我們會經常看到似是而非的現象,

這里沒有翻譯 compiler 為編譯器,因為它不僅僅代表編譯器,后續它會代表所有會導致指令重排序的機制,

如表 17.4-A 中所示,A 和 B 是共享屬性,r1 和 r2 是區域變數,初始時,令 A == B == 0,

表17.4-A. 重排序導致奇怪的結果 - 原始代碼

按照我們的直覺來說,r2 == 2 同時 r1 == 1 應該是不可能的,直觀地說,指令 1 和 3 應該是最先執行的,如果指令 1 最先執行,那么它應該不會看到指令 4 對 A 的寫入操作,如果指令 3 最先執行,那么它應該不會看到執行 2 對 B 的寫入操作,

如果真的表現出了 r22 和 r11,那么我們應該知道,指令 4 先于指令 1 執行了,

如果在執行程序出表現出這種行為( r22 和r11),那么我們可以推斷出以下指令依次執行:指令 4 => 指令 1=> 指令 2 => 指令 3,看上去,這種順序是荒謬的,

但是,Java 是允許 compilers 對指令進行重排序的,只要保證在單執行緒的情況下,能保證程式是按照我們想要的結果進行執行,即 compilers 可以對單執行緒內不產生資料依賴的陳述句之間進行重排序,如果指令 1 和指令 2 發生了重排序,如按照表17.4-B 所示的順序進行執行,那么我們就很容易看到,r22 和 r11 是可能發生的,

表 17.4-B. 重排序導致奇怪的結果 - 允許的編譯器轉換

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

一個簡單的編譯器優化操作是會復用 r2 的結果給 r5,因為它們都是讀取 r1.x,而且在單執行緒語意中,r2 到 r5之間沒有其他的相關的寫入操作,這種情況如表 17.4-D 所示,

Table 17.4-D

現在,我們來考慮一種情況,在執行緒1第一次讀取 r1.x 和 r3.x 之間,執行緒 2 執行 r6=p; r6.x=3; 編譯器進行了 r5復用 r2 結果的優化操作,那么 r2r50,r4 == 3,從程式員的角度來看,p.x 的值由 0 變為 3,然后又變為 0,

我簡單整理了一下:

例子結束,回到正題

Java 記憶體模型定義了在程式的每一步,哪些值是記憶體可見的,對于隔離的每個執行緒來說,其操作是由我們執行緒中的語意來決定的,但是執行緒中讀取到的值是由記憶體模型來控制的,

當我們提到這點時,我們說程式遵守執行緒內語意,執行緒內語意說的是單執行緒內的語意,它允許我們基于執行緒內讀操作看到的值完全預測執行緒的行為,如果我們要確定執行緒 t 中的操作是否是合法的,我們只要評估當執行緒 t 在單執行緒環境中運行時是否是合法的就可以,該規范的其余部分也在定義這個問題,

這段話不太好理解,首先記住“執行緒內語意”這個概念,之后還會用到,我對這段話的理解是,在單執行緒中,我們是可以通過一行一行看代碼來預測執行結果的,只不過,代碼中使用到的讀取記憶體的值我們是不能確定的,這取決于在記憶體模型這個大框架下,我們的程式會讀到的值,也許是最新的值,也許是過時的值,

此節描述除了 final 關鍵字外的java記憶體模型的規范,final將在之后的17.5節介紹,

這里描述的記憶體模型并不是基于 Java 編程語言的面向物件,為了簡潔起見,我們經常展示沒有類或方法定義的代碼片段,大多數示例包含兩個或多個執行緒,其中包含區域變數,共享全域變數或物件的實體欄位的陳述句,我們通常使用諸如 r1 或 r2 之類的變數名來表示方法或執行緒本地的變數,其他執行緒無法訪問此類變數,

17.4.1. 共享變數(Shared Variables)

所有執行緒都可以訪問到的記憶體稱為共享記憶體堆記憶體

所有的實體屬性,靜態屬性,還有陣列的元素都存盤在堆記憶體中,在本章中,我們用術語變數來表示這些元素,

區域變數、方法引數、例外物件,它們不會在執行緒間共享,也不會受到記憶體模型定義的任何影響,

兩個執行緒對同一個變數同時進行讀-寫操作寫-寫操作,我們稱之為“沖突”,

好,這一節都是廢話,愉快地進入到下一節

17.4.2. 操作(Actions)

這一節主要是講解理論,主要就是嚴謹地定義操作,

執行緒間操作是指由一個執行緒執行的動作,可以被另一個執行緒檢測到或直接影響到,以下是幾種可能發生的執行緒間操作:

  • 讀 (普通變數,非 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; // 這里永遠不會失敗
  }
}

17.4.3. 程式和程式順序(Programs and Program Order)

在每個執行緒 t 執行的所有執行緒間動作中,t 的程式順序是反映 根據 t 的執行緒內語意執行這些動作的順序 的總順序,

如果所有操作的執行順序 和 代碼中的順序一致,那么一組操作就是連續一致的,并且,對變數 v 的每個讀操作 r 會看到寫操作 w 寫入的值,也就是:

  • 寫操作 w 先于 讀操作 r 完成,并且
  • 沒有其他的寫操作 w' 使得 w' 在 w 之后 r 之前發生,

連續一致性對于可見性和程式執行順序是一個非常強的保證,在這種場景下,所有的單個操作(比如讀和寫)構成一個統一的執行順序,這個執行順序和代碼出現的順序是一致的,同時每個單個操作都是原子的,且對所有執行緒來說立即可見,

如果程式沒有任何的資料競爭,那么程式的所有執行操作將表現為連續一致,連續一致性 和/或 資料競爭的自由仍然允許錯誤從一組操作中產生,

完全不知道這句話是什么意思

如果我們用連續一致性作為我們的記憶體模型,那我們討論的許多關于編譯器優化和處理器優化就是非法的,比如在17.4-C中,一旦執行 p.x=3,那么后續對于該位置的讀操作應該是立即可以讀到最新值的,

連續一致性的核心在于每一步的操作都是原子的,同時對于所有執行緒都是可見的,而且不存在重排序,所以,Java 語言定義的記憶體模型肯定不會采用這種策略,因為它直接限制了編譯器和 JVM 的各種優化措施,

注意:很多地方所說的順序一致性就是這里的連續一致性,英文是 Sequential consistency

17.4.4. 同步順序(Synchronization Order)

每個執行都有一個同步順序,同步順序是由執行程序中的每個同步操作組成的順序,對于每個執行緒 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 的一系列規則,

17.4.5. Happens-before順序(Happens-before Order)

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,如果:

讀操作 r 不是先于 w 發生(比如不是 hb(r, w) ),同時沒有寫操作 w' 穿插在 w 和 r 中間(如不存在 hb(w, w') 和 hb(w', r)),非正式地,如果沒有 happens-before 關系阻止讀操作 r,那么讀操作 r 就能看到寫操作 w 的結果,

17.5. final 屬性的語意(final Field Semantics)

我們經常使用 final,關于它最基礎的知識是:用 final 修飾的類不可以被繼承,用 final 修飾的方法不可以被覆寫,用 final 修飾的屬性一旦初始化以后不可以被修改,

當然,這節說的不是這些,這里將闡述 final 關鍵字的深層次含義,

用 final 宣告的屬性正常情況下初始化一次后,就不會被改變,final 屬性的語意與普通屬性的語意有一些不一樣,尤其是,對于 final 屬性的讀操作,compilers 可以自由地去除不必要的同步,相應地,compilers 可以將 final 屬性的值快取在暫存器中,而不用像普通屬性一樣從記憶體中重新讀取,

final 屬性同時也允許程式員不需要使用同步就可以實作執行緒安全的不可變物件,一個執行緒安全的不可變物件對于所有執行緒來說都是不可變的,即使傳遞這個物件的參考存在資料競爭,

這可以提供安全的保證,即使是錯誤的或者惡意的對于這個不可變物件的使用,如果需要保證物件不可變,需要正確地使用 final 屬性域,

物件只有在構造方法結束了才被認為完全初始化了,如果一個物件完全初始化以后,一個執行緒持有該物件的參考,那么這個執行緒一定可以看到正確初始化的 final 屬性的值,

這個隱含了,如果屬性值不是 final 的,那就不能保證一定可以看到正確初始化的值,可能看到初始零值,

final 屬性的使用是非常簡單的:在物件的構造方法中設定 final 屬性;同時在物件初始化完成前,不要將此物件的參考寫入到其他執行緒可以訪問到的地方,如果這個條件滿足,當其他執行緒看到這個物件的時候,那個執行緒始終可以看到正確初始化后的物件的 final 屬性,

這里面說到了一個正確初始化的問題,看過《Java并發編程實戰》的可能對這個會有印象,不要在構造方法中將 this 發布出去,

這段代碼把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,

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;

17.5.1. final屬性的語意(Semantics of final Fields)

令 o 為一個物件,c 為 o 的構造方法,構造方法中對 final 的屬性 f 進行寫入值,當構造方法 c 退出的時候,會在final 屬性 f 上執行一個 freeze 操作,

注意,如果一個構造方法呼叫了另一個構造方法,在被呼叫的構造方法中設定 final 屬性,那么對于 final 屬性的 freeze 操作發生于被呼叫的構造方法結束的時候,

對于每一個執行,讀操作的行為被其他的兩個偏序影響,解參考鏈 dereferences() 和記憶體鏈 mc(),它們被認為是執行的一部分,這些偏序必須滿足下面的約束:

17.5.2. 在構造期間讀 final 屬性(Reading final Fields During Construction)

在構造物件的執行緒中,對該物件的 final 屬性的讀操作,遵守正常的 happens-before 規則,如果在構造方法內,讀某個 final 屬性晚于對這個屬性的寫操作,那么這個讀操作可以看到這個 final 屬性已經被定義的值,否則就會看到默認值,

17.5.3. final 屬性的修改(Subsequent Modification of final Fields)

在許多場景下,如反序列化,系統需要在物件構造之后改變 final 屬性的值,final 屬性可以通過反射和其他方法來改變,

唯一的具有合理語意的模式是:物件被構造出來,然后物件中的 final 屬性被更新,在這個物件的所有 final 屬性更新操作完成之前,此物件不應該對其他執行緒可見,也不應該對 final 屬性進行讀操作,

對于 final 屬性的 freeze 操作發生于構造方法的結束,這個時候 final 屬性已經被設值,還有通過反射或其他方式對于 final 屬性的更新之后,

即使是這樣,依然存在幾個難點,如果一個 final 屬性在屬性宣告的時候初始化為一個常量運算式,對于這個 final 屬性值的變化程序也許是不可見的,因為對于這個 final 屬性的使用是在編譯時用常量運算式來替換的,

另一個問題是,該規范允許 JVM 實作對 final 屬性進行強制優化,在一個執行緒內,允許對于 final 屬性的讀操作與構造方法之外的對于這個 final 屬性的修改進行重排序,

對于 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)中執行代碼塊,如果一個物件是在 final 屬性安全背景關系中構造出來的,那么在這個 final 屬性安全背景關系 中對于 final 屬性的讀操作不會和相應的對于 final 屬性的修改進行重排序,

final 屬性安全背景關系還提供了額外的保障,如果一個執行緒已經看到一個不正確發布的一個物件的參考,那么此執行緒可以看到了 final 屬性的默認值,然后,在 final 屬性安全背景關系中讀取該物件的正確發布的參考,這可以保證看到正確的 final 屬性的值,在形式上,在final 屬性安全背景關系中執行的代碼被認為是一個獨立的執行緒(僅用于滿足 final 屬性的語意),

在實作中,compiler 不應該將對 final 屬性的訪問移入或移出final 屬性安全背景關系(盡管它可以在這個執行背景關系的周邊移動,只要這個物件沒有在這個背景關系中進行構造),

對于 final 屬性安全背景關系的使用,一個恰當的地方是執行器或者執行緒池,在每個獨立的 final 屬性安全背景關系中執行每一個 Runnable,執行器可以保證在一個 Runnable 中對物件 o 的不正確的訪問不會影響同一執行器內的其他 Runnable 中的 final 帶來的安全保障,

17.5.4. 寫保護屬性(Write-Protected Fields)

通常,如果一個屬性是 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類中的代碼,

17.6. 字分裂(Word Tearing)

實作 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();
    }
}

這表明寫入位元組時不得覆寫相鄰的位元組,

17.7. double 和 long 的非原子處理 (Non-Atomic Treatment of double and long)

在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 來保證原子性,

來源:https://javadoop.com/post/Threads-And-Locks-md\

參考:
https://docs.oracle.com/javase/specs/
http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html
http://gee.cs.oswego.edu/dl/jmm/cookbook.html

近期熱文推薦:

1.1,000+ 道 Java面試題及答案整理(2022最新版)

2.勁爆!Java 協程要來了,,,

3.Spring Boot 2.x 教程,太全了!

4.別再寫滿屏的爆爆爆炸類了,試試裝飾器模式,這才是優雅的方式!!

5.《Java開發手冊(嵩山版)》最新發布,速速下載!

覺得不錯,別忘了隨手點贊+轉發哦!

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/501235.html

標籤:Java

上一篇:記錄解決安裝 golang easyjson json包遇到的坑(有庫但沒有可執行檔案)

下一篇:面向物件ooDay2

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more