Java併發知識分享
volatile的記憶體語義
從JSR-133(即從JDK1.5開始),volatile變數的寫-讀可以實現執行緒之間的通訊
當寫一個volatile變數時,JMM會把該執行緒對應的本地記憶體中的共享變數值重新整理到主記憶體。
當讀一個volatile變數時,JMM會把該執行緒物件的本地記憶體置為無效。執行緒接下來將從主記憶體中讀取共享變數。導致本地記憶體與主記憶體值一致。
volatile僅僅保證對單個volatile變數的讀/寫具有原子性,而鎖的互斥執行的特性可以確保整個臨界區程式碼的執行具有原子性。
concurrent包中,會發現一個通用的實現模式:
首先,宣告共享變數為volatile。
然後,使用CAS的原子條件更新來實現執行緒之間的同步。
同時,配置以volatile的讀/寫和CAS所具有的volatile讀和寫的記憶體語義來實現執行緒之間的通訊
final域的記憶體語義
對於final域,編譯器和處理器要遵守兩個重排序規則:
1.在建構函式內對一個final域的寫入,與隨後把這個構造物件的引用賦值給一個引用變數,這兩個操作之間不能重排序。
2.初次讀一個包含final域的物件的引用,與隨後初次讀這個final域,這兩個操作之間不能重排序。
注意:
1)JMM禁止編譯器把final域的寫重排序到建構函式之外
2)編譯器會在final域的寫之後,建構函式的return之前,插入一個Store屏障。這個屏障禁止處理器把final域的寫重排序到建構函式之外。
在建構函式內部,不能讓這個被構造物件的引用為其他執行緒所見,也就是物件引用不能在建構函式中“逸出”。
happens-before
happens-before是JMM最核心的概念。所以應該充分理解,不然你還學什麼java。
1. JMM的設計
1) 程式設計師對記憶體模型的使用。程式設計師希望記憶體模型易於理解、易於程式設計。程式設計師希望基於一個強記憶體模型來編寫程式碼
2)編譯器和處理器對記憶體模型的實現。束縛越少越好,這樣可以儘可能優化來提高效能。編譯器和處理器希望實現一個弱記憶體模型。
JSR-133 找到了這個平衡點
JMM把happens-before要求禁止的重排序分為下面兩類:
1)會改變程式執行結果的重排序
2)不會改變程式執行結果的重排序
JMM對這兩種性質的重排序,採取了不同的策略:
1)對於第一種,JMM要求編譯器和處理器必須禁止這種重排序。
2)對於第二種,JMM要求編譯器和處理器不做要求(JMM允許這種重排序)
JMM遵循一個基本原則:只要不改變程式的執行結果(指的是單執行緒程式和正確同步的多執行緒程式),編譯器和處理器怎麼優化都行。
2.happens-before定義
JSR-133 使用happens-before的概念來指定兩個操作之間的執行順序。由於這兩個操作可以在一個執行緒之內,也可以是在不同執行緒之間。因此,JMM可以通過happens-before關係向程式設計師提供跨執行緒的記憶體可見性保證。
JSR-133 中的定義:
1)如果一個操作happens-before另一個操作,那麼第一個操作的執行結果將對第二個操作可見,而且第一個操作的執行順序排在第二個操作之前。
2)兩個操作之間存在happens-before關係,並不意味著Java平臺的具體實現必須要按照happens-before關係指定的順序來執行。如果重排序之後的執行結果,與按happens-before關係來執行的結果一直,那麼這種重排序並不非法。
上面1)是對程式設計師的承諾
上面2)是對編譯器和處理器重排序的約束原則
as-if-serial語義保證單執行緒內程式的執行結果不被改變
happens-before保證正確同步的多執行緒的執行結果不被改變
其實都是幻覺,只不過JMM幫了你
3.happens-before規則
JSR-133 定義了happens-before規則:
1)程式順序規則:一個執行緒中的每個操作,happens-before於該執行緒中的任意後續操作。
2)監視器規則:對一個鎖的解鎖,happens-before於隨後對這個鎖的加鎖。
3)volatile變數規則:對一個volatile域的寫,happens-before於任意後續對這個volatile域的讀。
4)傳遞性:如果A happens-before B,且B happens-before C,那麼A happens-before C。
5)start()規則:如果執行緒A執行操作ThreadB.start()(啟動執行緒B),那麼A執行緒的ThreadB.start()操作happens-before於執行緒B中的任意操作。
6)join()規則:如果執行緒A執行操作ThreadB.join()併成功返回,那麼執行緒B中的任意操作happens-before與執行緒A從ThreadB.join()操作成功返回。
執行緒
簡介
作業系統排程的最小單元,多個執行緒能夠同時執行。執行緒都擁有各自的計數器、堆疊和區域性變數等屬性,並且能夠訪問共享的記憶體變數。由於處理器的高速切換,感覺是在同時執行。
可以檢視當前正在執行的執行緒的一些資訊
執行緒的優先順序
在Java執行緒中,通過一個整型成員變數priority來控制優先順序,優先順序的範圍從1-10,線上程構件的時候可以通過setPriority(int)方法來修改優先順序,預設優先順序是5,優先順序高的執行緒分配時間片的數量要多餘優先順序低的執行緒。
設定優先順序時,針對頻繁阻塞(休眠或者I/O操作)的執行緒需要設定較高優先順序,而偏重計算的執行緒則設定較低的優先順序,確保處理器不會被獨佔。在不同的JVM以及作業系統上,執行緒規劃會存在差異。
只是一種對處理器的建議,並不能保證優先順序高的一定優先執行。
執行緒的狀態
Java執行緒在執行的生命週期中可能處於6種不同的狀態,在給定的一個時刻,執行緒只能處於其中一個狀態。
NEW:初始狀態,執行緒被構建,但是還沒有呼叫start()方法
RUNNABLE:執行狀態,Java執行緒將作業系統中的就緒和執行兩種狀態籠統地稱作“執行中”
BLOCKED:阻塞狀態,表示執行緒阻塞於鎖
WAITING:等待狀態,表示執行緒進入等待狀態,進入該狀態表示當前執行緒需要等待其他執行緒做出一些特定動作(通知或中斷)
TIME_WAITING:超時等待狀態,該狀態不同於WAITING,它是可以在指定的時間自行返回的
TERMINATED:終止狀態,表示當前執行緒已經執行完畢
Daemon執行緒
Daemon執行緒是一種支援型執行緒,因為它主要被用作程式中後臺排程以及支援性工作。這意味著,當一個Java虛擬機器中不存在非Daemon執行緒的時候,Java虛擬機器將會退出。可以通過呼叫Thread.setDaemon(true)將執行緒設定為Daemon執行緒。
注意:Daemon屬性需要在啟動執行緒之前設定,不能在啟動執行緒之後設定。並不能保證finally程式碼一塊一定會執行
啟動和終止執行緒
1.構造執行緒
在執行執行緒之前首先要構造一個執行緒物件,執行緒物件在構造的時候需要提供執行緒所需要的屬性,如執行緒組、執行緒優先順序、是否是Daemon執行緒等資訊
2.啟動執行緒
呼叫start()方法就可以啟動這個執行緒。最好給執行緒設定個名字,排查問題好排查。
3.中斷
中斷可以理解為執行緒的一個標識位屬性,它表示一個執行中的執行緒是否被其他執行緒進行了中斷。其他執行緒通過interrupt()方法對其進行中斷操作。
執行緒通過檢查自身是否被中斷來進行響應,執行緒通過方法isInterrupted()來進行判斷是否被中斷,也可以呼叫靜態方法Thread.interrupted()對當前執行緒的中斷標識進行復位。如果該執行緒已經處於終結狀態,即使該執行緒被中斷過,在呼叫該執行緒物件的interrupted()時依舊會返回false。
4.安全地終止執行緒
中斷操作時一種簡便的執行緒間互動方式,而這種互動方式最適合用來你取消或停止任務。
執行緒間通訊
1.volatile和synchronized關鍵字
關鍵字volatile可以用來修飾字段(成員變數),就是告知程式對該變數的訪問用從共享記憶體中獲取,而對它的改變必須同步重新整理回共享記憶體,它能保證所有執行緒對變數訪問的可見性。過多地使用volatile是不必要的,因為它會降低程式執行的效率。
關鍵字synchronized可以修飾方法或者以同步塊的形式來進行使用,它主要確保多個執行緒在同一時刻,只能有一個執行緒處於方法或同步程式碼塊中,它保證了執行緒對變數訪問的可見性和排他性。
任意一個物件都有自己的監視器,當這個物件由同步程式碼塊或者這個物件的同步方法呼叫時,執行方法的執行緒必須先獲取到該物件的監視器才能進入同步塊或者同步方法,而沒有獲取到監視器的執行緒將會被阻塞在同步塊和同步方法的入口處,進入BLOCKED狀態。
如果獲取監視器失敗,執行緒進入同步佇列,執行緒狀態變為BLOCKED。當訪問Object前驅釋放了鎖,則 該釋放操作喚醒阻塞在同步佇列中的執行緒,使其重新嘗試對監視器的獲取。
2.等待/通知機制
一個執行緒A呼叫了物件O的wait()方法進入等待狀態,而另一個執行緒B呼叫了物件O的notify()方法或者notifyAll()方法,執行緒A收到通知後從物件O的wait()方法返回,進而執行後續操作。
兩個執行緒通過物件來完成互動,而物件上的wait()和notify/notifyAll()的關係就如同開關訊號一樣,用來完成等待方和通知方之間的互動工作。
細節:
1.使用wiat、notify、notifyAll時需要先對呼叫物件加鎖
2.呼叫wait方法後,執行緒狀態由RUNNING變為WAITING,並將當前執行緒放置到物件的等待佇列。
3.notify或notifyAll方法呼叫後,等待執行緒依舊不會從wait返回,需要呼叫notify或notifyAll的執行緒釋放鎖之後,等待執行緒才有機會從wait返回
4.notify方法將等待佇列中的一個等待執行緒從等待佇列中移到同步佇列中,而notifyAll方法則是將等待佇列中的所有執行緒全部移到同步佇列,被移動的執行緒狀態由WAITING變為BLOCKED
5.wait方法返回的前提是獲得了呼叫物件的鎖
從上述細節中可以看到,等待/通知機制依託於同步機制,其目的就是確保等待執行緒從wait方法返回時能夠感知到通知執行緒對變數做出的修改。
等待/通知的經典範式
等待方遵循如下原則
1)獲取物件的鎖
2)如果條件不滿足,那麼呼叫物件的wait方法,被通知後仍要檢查條件
3)條件滿足則執行對應的邏輯
對應的虛擬碼:
synchronized(物件){
while(條件不滿足){
物件.wait()
}
對應的處理邏輯
}
通知方遵循如下原則
1)獲得物件的鎖
2)改變條件
3)通知所有等待在物件上的執行緒
對應的虛擬碼:
synchronized(物件){
改變條件
物件.notifyAll()
}
3.Thread.join()的使用
如果一個執行緒A執行了thread.join()語句,其含義是:當前執行緒A等待thread執行緒終止之後才從thread.join()返回。
變相的等待/通知模型
4.ThreadLocal
ThreadLocal,即執行緒變數,是一個以ThreadLocal物件為鍵;任意物件為值的儲存結構。
一個執行緒上可以綁上多個ThreadLocal
非常實用的一個功能。