Java高併發程式設計入門
說在前面
本文絕大部分參考《JAVA高併發程式設計》,類似讀書筆記和擴充套件。
走入並行世界
概念
同步(synchronous)與非同步(asynchronous)
同步和非同步通常來形容一次方法呼叫。同步方法呼叫一旦開始,呼叫者必須等到方法呼叫返回後,才能繼續執行任務。
非同步方法更像一個訊息傳遞,一旦開始,方法呼叫就會立即返回,呼叫者就可以繼續後續的工作。非同步方法通常會在另外的執行緒中“真實”的執行。整個過程不會阻礙呼叫者的工作。
併發(concurrency)和並行(parallelism)
連結:併發Concurrent與並行Parallel的區別
臨界區
臨界區表示一種公共資源或者說是共享資源,可以被多個執行緒使用。但是每一次只能有一個執行緒使用它,一旦臨界區資源被佔用,其他執行緒要想得到這個資源就必須等待。
在並行程式中。臨界區資源是保護物件。就比如大家公用的一臺印表機,必然是一個人打完另一個人的才能列印,否則就會出亂子。
阻塞(blocking)與非阻塞(non-blocking)
阻塞和非阻塞通常來形容多執行緒間的相互影響。比如一個執行緒佔用了臨界區資源,那麼其他所有需要這個資源的執行緒都需要在臨界區中等待。等待會導致執行緒掛起,這種情況就是阻塞。此時如果佔用這個資源的執行緒一直不願釋放資源,那麼其他所有阻塞在這個臨界區上的執行緒都不能工作。
反之就是非阻塞,它強調沒有一個執行緒可以妨礙其他執行緒執行。所有執行緒都會嘗試不斷前向執行。
死鎖(deadlock)、飢餓(starvation)和活鎖(livelock)
這三種情況都屬於執行緒活躍性問題。如果發現上述情況,那麼相關執行緒可能就不再活躍,也就是說它可能很難再繼續執行任務了。
1 死鎖
應該是最糟糕的情況之一。它們彼此相互都佔用著其他執行緒的資源,都不願釋放,那麼這種狀態將永遠維持下去。
死鎖是一個很嚴重的問題,應該避免和小心。就如4輛小汽車,互相都佔用對方的車道,無法正常行駛。
2 飢餓
是指一個或多個執行緒因為種種原因一直無法得到所需要的資源,導致一直無法執行,比如它的執行緒優先順序太低,高優先順序的執行緒一直搶佔它所需要的資源。另一種可能是某一個執行緒一直佔用著關鍵資源不放,導致其他需要這個資源的執行緒一直無法得到這個資源,無法正常執行。與死鎖相比,飢餓還是可能在一段時間內解決的,比如高優先順序的執行緒執行完任務後,不在搶佔資源,資源得到釋放。
3 活鎖
是非常有趣的情況,也是最難解決的情況。這就比如,大家在一個兩人寬的橋上走路,雙方都很有禮貌。都在第一時間禮讓對方,一個往左一個往右,導致兩人都無法正常通行。放到執行緒中,就體現為,兩個執行緒都拿到資源後都主動釋放給他人使用,那麼就會出現資源不斷的在兩個執行緒中跳動,而沒有一個執行緒可以拿到資源後正常執行,這個就是活鎖。
併發級別
由於臨界區的存在,多執行緒之間的併發必須受到控制。根據控制併發的策略,我們可以把併發的級別進行分類,大致上可以分為阻塞、無飢餓、無障礙,無鎖和無等待幾種。
阻塞(blocking)
一個執行緒是阻塞的,那麼在其他執行緒釋放資源之前,當前執行緒無法繼續執行。當我們使用synchronized關鍵字,或者重入鎖時,我們得到的就是阻塞的執行緒。
無論是synchronized還是重入鎖,都會在檢視執行後續程式碼前得到臨界區的鎖,如果得不到,執行緒就會被掛起等待,直到佔有了所需要的資源為止。
無飢餓
如果執行緒間是有優先順序的,那麼執行緒呼叫總是會傾向於滿足高優先順序的執行緒。也就是說對同一個資源的分配是不公平的。對於非公平的鎖來說,系統允許高優先順序的執行緒插隊,這樣有可能導致低優先順序的執行緒產生飢餓。但如果鎖是公平的,滿足先來後到,那麼飢餓就不會產生,不管新來的執行緒優先順序多高,要想獲得資源就必須排隊。那麼所有的執行緒都有機會執行。
無障礙(obstruction-Free)
無障礙是一種最弱的非阻塞排程。兩個執行緒如果是無障礙的執行,那麼他們不會因為臨界區的問題導致一方被掛起。大家都可以大搖大擺進入臨界區工作。那麼如果大家都修改了共享資料怎麼辦呢?對於無障礙的執行緒來說,一旦出現這種情況,當前執行緒就會立即對修改的資料進行回滾,確保資料安全。但如果沒有資料競爭發生,那麼執行緒就可以順利完成自己的工作,走出臨界區。
如果阻塞控制的方式比喻成悲觀策略。也就是說系統認為兩個執行緒之間很有可能發生不幸的衝突,因此,保護共享資料為第一優先順序。相對來說,非阻塞的排程就是一種樂觀策略,他認為多執行緒之間很有可能不會發生衝突,或者說這種概率不大,但是一旦檢測到衝突,就應該回滾。
從這個策略來看,無障礙的多執行緒程式不一定能順利執行。因為當臨界區的字眼存在嚴重的衝突時,所有執行緒可能都進行回滾操作,導致沒有一個執行緒可以走出臨界區。所以我們希望在這一堆執行緒中,至少可以有一個執行緒可以在有限時間內完成自己的操作,至少這可以保證系統不會再臨界區進行無線等待。
一種可行的無障礙實現可以依賴一個“一致性標記”來實現。執行緒在操作之前,先讀取並保持這個標記,在操作完後,再次讀取,檢查這個標記是否被修改過,如果前後一致,則說明資源訪問沒有衝突。如果不一致,則說明資源可能在操作過程中與其他寫執行緒衝突,需要重試操作。任何對保護資源修改之前,都必須更新這個一致性標記,表示資料不安全。
無鎖(lock-free)
無鎖的並行都是無障礙的。在無鎖的情況下,所有的執行緒都能嘗試對臨界區的資源進行訪問,但不同的是,無鎖的併發保證必然有一個執行緒能夠在有限步內完成操作離開臨界區。
在無鎖的排程中,一個典型的特點是可能會包含一個無窮迴圈。在這個迴圈中線性不斷嘗試修改共享資料。如果沒有衝突,修改成功,那麼執行緒退出,否則嘗試重新修改。但無論如何,無鎖的並行總能保證有一個執行緒可以勝出,不至於全軍覆沒。至於臨界區中競爭失敗的執行緒,則不斷重試。如果運氣不好,總是不成功,則會出現類似飢餓的現象,執行緒會停止不前。
無等待(wait-free)
無鎖是要求至少有一個執行緒在有限步內完成操作,而無等待則是在無鎖的基礎之上進一步擴充套件。他要求所有執行緒都必須在有限步內完成操作。這樣就不會引起飢餓問題。如果限制這個步驟上限,還可以分為有界無等待和執行緒無關的無等待幾種,它們之間的區別只是對迴圈次數的限制不同。
一種典型的無等待結構是RCU(read-copy-update)。它的基本思想是,對資料的讀可以不加控制,因此所有讀執行緒都是無等待的,它們既不會被鎖定等待也不會引起任何衝突。但在寫資料時,先取得原始資料的副本,接著只修改副本資料,修改完後,在合適的時機回寫資料。
有關並行的兩個重要定律
Amdahl定律
加速比定義:加速比= 優化前系統耗時/優化後系統耗時
根據Amdahl定律,使用多核CPU對系統進行優化,優化的效果取決於CPU的數量以及系統中序列程式的比重。CPU數量越多,序列化比重越低,則優化效果越好。僅提高CPU核數不降低系統序列程式比重,也無法提高系統性能。
Gustafson定律
根據Gustafson定律,我們更容易發現,如果序列化比例很小,並行化比例很大,那麼加速比就是處理器的個數。只要不斷增加CPU核數,就可以提高系統性能。
JAVA記憶體模型(JMM)
由於併發程式要比序列程式複雜的多,其中一個重要的原因是併發程式下資料訪問的一致性和安全性將受到嚴重的挑戰。因此我們需要在深入瞭解並行機制之前,再定義一種規則,保證多執行緒程式可以有效的,正確的協同工作。而JMM也就為此而生。JMM的關鍵技術點都是圍繞多執行緒的原子性、可見性和有序性來建立的。
原子性(atomicity)
指一個操作是不可中斷的。即使多個執行緒一起執行的時候,一個操作一旦開始,就不會被其他執行緒干擾。
比如對一個靜態變數int i賦值,A執行緒賦值1,B執行緒賦值-1,那麼這個變數i的結果可能是1或者-1,沒有其他情況。這就是原子性。
但如果是給一個long型賦值的話就沒那麼幸運了。在32位系統下,long型資料的讀寫不是原子性的(因為long有64位)。
在32位的java虛擬機上執行如下例子,就會出現非原子性的問題了。
public class E1 {
public static long t=0;
public static class ChangT implements Runnable{
private long to;
public ChangT(long to) {
this.to = to;
}
@Override
public void run() {
while (true){
E1.t = to;
Thread.yield();
}
}
}
public static class ReadT implements Runnable{
@Override
public void run() {
while (true){
long tmp = E1.t;
if (tmp != 111L && tmp != -999L && tmp != 333L && tmp != -444L)
System.out.println(tmp);
Thread.yield();
}
}
}
public static void main(String[] a){
new Thread(new ChangT(111L)).start();
new Thread(new ChangT(-999L)).start();
new Thread(new ChangT(333L)).start();
new Thread(new ChangT(-444L)).start();
new Thread(new ReadT()).start();
}
}
理想的結果可能是什麼都不輸出,但是,一旦執行,就會有大量的輸出一下資訊
...
-4294966963
4294966852
-4294966963
...
我們可以看到讀取執行緒居然讀取到不可能存在的資料。因為32為系統中的long型資料的讀和寫不是原子性的,多執行緒之間互相干擾了。
如果我們給出結果中幾個數值的2進位制,大家就會更清晰的認識了。
-999 = 1111111111111111111111111111111111111111111111111111110000011001
-444 = 1111111111111111111111111111111111111111111111111111111001000100
111 = 0000000000000000000000000000000000000000000000000000000001101111
333 = 0000000000000000000000000000000000000000000000000000000101001101
4294966852 = 0000000000000000000000000000000011111111111111111111111001000100
-4294967185 = 1111111111111111111111111111111100000000000000000000000001101111
上面這幾個數值的補碼形式,也是在計算機內真實儲存的內容。不難發現4294966852其實是111或333的前32為夾雜著-444的後32位的資料。而-4294967185其實是-999或-444夾雜111後32位的資料。換句話說,由於並行的關係數字被寫亂了。或者讀的時候讀串位了。
通過這個例子,大家應該對原子性應該有基本的認識。
可見性(visibility)
可見性是指當一個執行緒修改了一個共享變數。其他執行緒是否可以立即知道這個修改。對於序列程式來說這個問題是不存在的。但這個問題在並行程式中就很有可能出現。如果一個執行緒修改了某一個全域性變數。其他執行緒未必可以馬上知道這個修改。如果CPU1和CPU2上各運行了一個執行緒,它們共享變數t。由於編譯器優化或者硬體優化緣故。在CPU1上的執行緒將變數t進行了優化,將其快取在cache中或者暫存器裡。這種情況下如果CPU2上的某個執行緒修改了t的實際值,那麼CPU1上的執行緒可能就無法意識到這個改動,依舊會讀取cache或者暫存器中的舊值。因此就產生了可見性的問題。可見性問題在並行程式中也是需要重點關注的問題之一。
可見性問題是一個綜合性問題,處理上述提到的快取優化和硬體優化會導致可見性問題外,指令重排以及編譯器的優化,都有可能導致這個問題。
附兩個例子便於理解可見性問題。
有序性(ordering)
有序性是三個問題中最難理解的,對於一個執行緒的執行程式碼而言,我們總是習慣性的認為程式碼的執行是從先往後的,依次執行的。這麼理解也不能完全說是錯誤的。在一個執行緒的情況下確實是從先往後。但是在併發時,程式的執行就可能出現亂序,寫在前面的程式碼可能會後執行。
有序性的問題的原因是因為程式在執行的時候,可能發生指令重排,重排後的指令和原指令的順序未必一致。
指令重排有一個基本的前提是,保證序列語義的一致性。指令重排不會使序列的語義邏輯發生問題。因此在序列程式碼中不必擔心這個問題。而在多執行緒間就無法保證了。
so,問題來了。為什麼會指令重排呢?
這完全是基於效能考慮。
我們知道一條指令的執行是可以分很多步驟的。簡單的說可以分如下幾步:
- 取指 IF
- 譯碼和取暫存器運算元 ID
- 執行或者有效地址計算 EX
- 儲存器訪問 MEM
- 回寫 WB
我們的彙編指令也不是一步就執行完了。在CPU的實際工作中,還是要分幾步去執行的。當然,每個步驟涉及的硬體也可能不同。比如,取指會用到PC暫存器和儲存器,譯碼會用到指令暫存器組,執行會使用ALU(算術邏輯單元(arithmetic and logic unit) 是能實現多組算術運算和邏輯運算的組合邏輯電路,簡稱ALU。主要功能是二進位制算數運算),寫回時需要暫存器組。
由於一個步驟可能使用不同的硬體完成,因此,就發明了流水線技術來執行指令。
- 指令1 IF ID EX MEM WB
- 指令2 IF ID EX MEM WB
可以看到,到兩條指令執行時,第一條指令其實還未執行完,這樣的好處是,假設每一步需要1毫秒,那麼第2條指令需要等待5毫秒才能執行。而通過流水線技術,指令2就只需等待1毫秒。這樣有了流水線就可以讓CPU高效的執行。但是,流水線總是害怕被中斷。流水線滿載的時候效能確實相當不錯,但是一旦中斷,所有硬體裝置都會進入停頓期,再次滿載又需要幾個週期,因此效能損失會比較大,所以我們就需要想辦法來不讓流水線中斷。
之所以需要指令重排就是避免流水線中斷,或儘量少的中斷流水線。當然指令重排只是減少中斷的一種技術,實際上CPU設計中,我們還有更多的軟硬體技術來防止中斷。具體大家就自己探究吧。
通過例子我們加深下理解。
示例 1 :
A = B + C執行過程。
左邊是彙編指令,LW表示load,其中LW R1,B表示把B的值載入到R1暫存器中。ADD就是加法,把R1,R2的值想加放到R3中。SW表示store,就是將R3暫存器的值儲存到變數A中。
//A = B + C 執行過程
LW R1,B IF ID EX MEM WB
LW R2,C IF ID EX MEM WB
ADD R3,R1,R2 IF ID X EX MEM WB
SW A,R3 IF X ID EX MEM WB
左邊是指令由上到下執行,右邊是流水線情況。在ADD上的大叉表示一箇中斷。因為R2中的資料還沒準備好,所以ADD操作必須進行一次等待。由於ADD的延遲,後面的指令都要慢一拍。
示例 2 :
a = b + c ;
d = e - f ;
執行過程如下
其實就是將中斷的時間去做別的事情,如load資料。這樣時間就可以規劃銜接好。有點兒像專案管理中優化關鍵路徑。由此可見,指令重排對於提高CPU處理效能是十分必要的,雖然確實帶來了亂序的問題,但這點兒犧牲完全值得的。
雖然java虛擬機器和執行系統會對指令進行一定的重排,但是指令重排是有原則的。
- 原則基本包括以下:
1 程式順序原則:一個執行緒內保證語義的序列性
Eg:
a=1;
b=a+1;
第二條語句依賴於第一條執行結果。所以不允許指令重排。
2 volatile規則:volatile變數的寫,先發生與讀,這保證了volatile變數的可見性。
3 鎖規則:解鎖(unlock)必然發生在隨後的加鎖(lock)前
Eg:
鎖規則強調,unlock操作必然發生在後續的對同一個鎖的lock之前,也就是說,
如果對一個鎖解鎖後,在加鎖,那麼加鎖的動作絕對不能重排到解鎖動作之前。
很顯然,如果這麼做,加鎖行為是無法獲得這把鎖的。
4 傳遞性:A先於B,B先於C,那麼A必然先於C
5 執行緒的start()方法先於它的每一個動作
6 執行緒的所有操作先於執行緒的終結(Thread.join())
7 執行緒的中斷(interrupt())先於被中斷執行緒的程式碼
8 物件的建構函式執行、結束先於finalize()方法
基礎
執行緒生命週期
執行緒所有的狀態都在Thread.State列舉類中定義
public enum State {
/**
* 表示剛剛建立的執行緒,這種執行緒還沒開始執行。
**/
NEW,
/**
* 呼叫start()方法後,執行緒開始執行,處於RUNNABLE狀態,
* 表示執行緒所需要的一切資源以及準備好。
**/
RUNNABLE,
/**
* 當執行緒遇到synchronized同步塊,就進入了BLOCKED阻塞狀態。
* 這時執行緒會暫停執行,直到獲得請求的鎖。
**/
BLOCKED,
/**
* WAITING和TIMED_WAITING都表示等待狀態,他們是區別是WAITING表示進入一個無時間限制的等待
* TIMED_WAITING會進入一個有時間限制的等待。
* WAITING的狀態正是在等待特殊的事件,如notify()方法。而通過join()方法等待的執行緒,則是等待目標執行緒的終止。
* 一旦等到期望的時間,執行緒就會繼續執行,進入RUNNABLE狀態。
* 當執行緒執行完後進入TERMINATED狀態,表示執行緒執行結束。
**/
WAITING,
TIMED_WAITING,
TERMINATED;
}
執行緒的基本操作
啟動初始化及基本方法
參考多執行緒基礎
終止執行緒
一個執行緒執行完後會結束,無須手動關閉,但是如一些系統性服務的執行緒基本都是一個大的迴圈,一般情況不會終止。
如何才能正常關閉執行緒呢?JDK提供了一個Thread.stop方法就可以立即關閉一個執行緒。但是這個方法太暴力,基本不會使用。並且stop()方法也是標記要廢棄的方法。stop()強行的將執行中的執行緒關閉,可能會造成資料不一致問題。
看圖說話:
舉個栗子:
public class ThreadStopExample {
public static User u = new User();
public static void main(String[] a){
/**
* 開啟讀取執行緒
*/
new Thread(new readObj(),"讀--執行緒").start();
while (true){
Thread t = new Thread(new changeObj(),"寫--執行緒");
t.start();
try {
/**
* 主執行緒sleep 150毫秒,處理業務
*/
Thread.sleep(150);
} catch (InterruptedException e) {
e.printStackTrace();
}
/**
* 將寫執行緒停止
*/
t.stop();
}
/**
* 執行結果:
* 觀察這些值,name屬性永遠比id小,是因為它永遠是上一次的值,就是因為stop(),無法完整的完成id和name賦值.
*
* 為什麼會不一致呢?
* 因為 User 通過 changeObj()方法不斷改變,當changeObj方法設定id後,需要處理其他花費100毫秒的業務.完成後設定name的值.
* 在這100毫秒中,呼叫changeObj()的主執行緒恰好執行了stop()方法,
* 雖然已經設定了User的id屬性值,但User的name屬性依然是上次迴圈的值.沒來得及賦值就stop()了.
* 所以這就是為什麼stop()會產生不一致問題.
*
* User{id=1455613327, name='1455613326'}
* User{id=1455613329, name='1455613328'}
* User{id=1455613331, name='1455613330'}
* User{id=1455613331, name='1455613330'}
* User{id=1455613331, name='1455613330'}
* .......
*/
}
/**
* 修改操作
*/
public static class changeObj implements Runnable{
@Override
public void run() {
while (true){
synchronized(u){
int v = (int) (System.currentTimeMillis()/1000);
u.setId(v);
try {
/**
* sleep 100毫秒,處理業務
*/
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
u.setName(String.valueOf(v));
}
Thread.yield();
}
}
}
/**
* 讀取操作
*/
public static class readObj implements Runnable{
@Override
public void run() {
while (true) {
synchronized (u) {
/**
* 當ID 不等於 name時,列印.
*
*/
if (u.getId() != Integer.parseInt(u.getName())){
System.out.println(u);
}
}
Thread.yield();
}
}
}
public static class User{
private int id ;
private String name ;
//getter setter
public User() {
this.id = 0;
this.name = "0";
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
}
如何正確的stop,如何不寫壞物件,請看修改後的程式碼如下,我們採用自己的方式去達到執行緒stop,當然還有其他更好的方案。
public static class changeObj implements Runnable{
//定義一個stop標識來實現我們自己的關閉方法
volatile static boolean stopMe = false;
@Override
public void run() {
while (true){
//增加if塊
if (stopMe){
System.out.println("exit by stopMe...");
break;
}
synchronized(u){
...
}
...
}
}
}
public static void main(String[] a){
while (true){
...
//t.stop();
changeObj.stopMe = true;
}
}
執行緒中斷
執行緒中斷是重要的執行緒協作機制,中斷就是讓執行緒停止執行,但這個停止執行非stop()的暴力方式。JDK提供了更安全的支援,就是執行緒中斷。
執行緒中斷並不會使執行緒立即停止,而是給執行緒傳送一個通知,告訴目標執行緒有人希望你退出。至於目標執行緒接到通知後什麼時候停止,完全由目標執行緒自行決定。這點很重要,如果執行緒接到通知後立即退出,我們就又會遇到類似stop()方法的老問題。
與執行緒有關的三個方法,
1、中斷執行緒
public void Thread.interrupt()
說明:Thread.interrupt() 是一個例項方法,他通知目標執行緒中斷,也就是設定中斷標誌位。中斷標誌位表示當前執行緒已經被中斷了。
2、判斷是否被中斷
public boolean Thread.isInterrupted()
說明:Thread.isInterrupted() 也是例項方法,他判斷當前執行緒是否被中斷(通過檢查中斷標誌位)
3、判斷是否被中斷,並清除當前中斷狀態
public static boolean Thread.interrupted()
說明:Thread.interrupted() 是靜態方法,判斷當前執行緒的中斷狀態,但同時會清除當前執行緒的中斷標誌位狀態。
例項1
看起來和stopMe的手法一樣,但是中斷功能更為強勁,比如遇到sleep()或wait()這樣的操作時,就只能用中斷標識了。
public class InterruptExample {
public static void main(String [] a){
Thread t1 = new Thread("執行緒小哥 - 1 "){
@Override
public void run() {
while (true){
/**
* 必須得判斷是否接受到中斷通知,如果不寫退出方法,也無法將當前執行緒退出.
*/
if (Thread.currentThread().isInterrupted()){
System.out.println(Thread.currentThread().getName() + " Interrupted ... ");
break;
}
Thread.yield();
}
}
};
t1.start();
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
/**
* 給目標執行緒傳送中斷通知
* 目標執行緒中必須有處理中斷通知的程式碼
* 否則,就算髮送了通知,目標執行緒也無法停止.
*/
t1.interrupt();
}
}
例項2
public class InterruptExample {
public static void main(String [] a){
Thread t1 = new Thread("執行緒小哥 - 1 "){
@Override
public void run() {
while (true){
/**
* 必須得判斷是否接受到中斷通知,如果不寫退出方法,也無法將當前執行緒退出.
*/
if (Thread.currentThread().isInterrupted()){
System.out.println(Thread.currentThread().getName() + " Interrupted ... ");
break;
}
try {
/**
* 處理業務邏輯花費10秒.
* 而在這時,主執行緒傳送了中斷通知,當執行緒在sleep的時候如果收到中斷
* 則會丟擲InterruptedException,如果在異常中不處理,則執行緒不會中斷.
*
*/
Thread.sleep(10000);
} catch (InterruptedException e) {
System.out.println("我錯了....");
/**
* 在sleep過程中,收到中斷通知,丟擲異常.可以直接退出執行緒.
* 但如果還需要處理其他業務,則需要重新中斷自己.設定中斷標記位.
* 這樣在下次迴圈的時候 執行緒發現中斷通知,才能正確的退出.
*/
Thread.currentThread().interrupt();
}
Thread.yield();
}
}
};
t1.start();
try {
/**
* 處理業務500毫秒
* 然後傳送中斷通知,此時t1執行緒還在sleep中.
*/
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
/**
* 給目標執行緒傳送中斷通知
* 目標執行緒中必須有處理中斷通知的程式碼
* 否則,就算髮送了通知,目標執行緒也無法停止.
*/
t1.interrupt();
}
}
等待(wait)和通知(notify)
為了支援多執行緒之間的協作,JDK提供了兩個非常重要的等待方法wait()和nofity()方法。這兩個方法並不是Thread類中的,而是Object類,這意味著任何物件都可以呼叫這兩個方法。
比如執行緒A呼叫了obj.wait()方法,那麼執行緒A就會停止執行而轉為等待狀態,進入obj物件的等待佇列。這個等待佇列可能有多個執行緒,因為系統執行多個執行緒同時等待同一個物件。其他執行緒呼叫obj.notify()方法時,它就會從等待佇列中隨機選擇一個執行緒並將其喚醒。注意著個選擇是不公平的,是隨機的。
obj.wait()方法並不是可以隨便呼叫。他必須包含在對應的synchronized語句中。無論是wait還是notify都必須首先獲得目標物件的一個監視器。而正確執行wait方法後,會釋放這個監視器,這樣其他等待obj上的執行緒才能獲得這個監視器,不至於全部無法執行。
在呼叫obj.notify()前,同樣也必須獲得obj的監視器,索性wait方法已經釋放了監視器。喚醒某個執行緒後(假設喚醒了A),A執行緒要做的第一件事並不是執行後續的程式碼,而是要嘗試重新獲得obj監視器。而這個監視器也正是A執行wait方法前所只有的那個obj監視器。如果暫時無法獲得。A還必須要等待這個監視器。當A獲得監視器後,才能真正意義上的繼續執行。
注意:wait方法和sleep方法都可以讓執行緒等待若干時間,處理wait方法可以喚醒之外,另外一個主要區別是wait方法會釋放目標物件的鎖,而sleep方法不會釋放。
例子:
public class WaitNotifyExample {
public static void main (String [] a){
Thread a1 = new A();
Thread b1 = new B();
a1.start();
b1.start();
/**
* 執行結果:
* A start ...
* A wait for obj ...
* B start ... notify one Thread...
* B end
* 這裡間隔2秒
* A end
* */
}
final static Object obj = new Object();
public static class A extends Thread{
@Override
public void run() {
synchronized (obj){
System.out.println("A start ... ");
try {
System.out.println("A wait for obj ... ");
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("A end");
}
}
}
public static class B extends Thread{
@Override
public void run() {
synchronized (obj){
System.out.println("B start ... notify one Thread...");
obj.notify();
System.out.println("B end");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
掛起(suspend)和繼續執行(resume)執行緒
這兩個方法雖然已經不推薦使用了。但是這裡再提一下,不推薦使用suspend掛起執行緒是因為suspend掛起執行緒後不釋放鎖資源,導致其他執行緒想要訪問這個鎖資源時都會被等待。無法正常執行。而suspend掛起的執行緒居然還是RUNNABLE狀態,這也嚴重影響了我們隊系統當前狀態的判斷。
示例
public class SuspendExample {
public static Object u = new Object();
static ChangeObj c1 = new ChangeObj("T1");
static ChangeObj c2 = new ChangeObj("T2");
public static class ChangeObj extends Thread{
public ChangeObj(String name) {
super(name);
}
@Override
public void run() {
synchronized (u) {
System.out.println("Thread in : " + getName());
/* //註釋1
try {
System.out.println("sleep 500ms : " + getName());
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
//進入的執行緒掛起,且不釋放資源
Thread.currentThread().suspend();
System.out.println("resume by : " + getName());
}
}
}
public static void main(String[] a) throws InterruptedException {
//啟動c1執行緒
c1.start();
/**
* 主執行緒工作100毫秒,(非常關鍵)
* 這裡的意思是
* 第一:為了演示,保證c1能搶佔到資源,讓主執行緒sleep後再啟動c2
* 第二:保證c1能在執行resume的時候執行完成.這樣才能保證c1本身可以有效釋放資源.
* 假設c1中執行業務耗時500毫秒後 才執行suspend.(將註釋[1]放開).而主執行緒僅僅sleep100毫秒後執行了c1.resume().
* 這樣就導致c1無法釋放鎖,結果列印的是
* Thread in : T1
* sleep 500ms : T1
* 無法再繼續走下去.
*/
Thread.sleep(100);
//啟動c2執行緒,但在c1不釋放資源的情況下,c2只能等待.
c2.start();
//c1 釋放鎖,此時c1應該已經執行了suspend掛起狀態,resume繼續執行
c1.resume();
/**
* 解決c2掛起無法繼續的方法:
* 1 將主執行緒sleep1000毫秒,保證c1在1000毫秒內執行完成,
* 但是這不是最好的方法,因為c1有可能在1000毫秒內執行不完
* Thread.sleep(1000);
* 2 將c2.resume() 放到c1.join後面.
*/
//c2 繼續執行,其實這裡提前執行了resume.導致c2在掛起後無法resume.
//因為c1.join導致c2必須在c1執行完後才能執行.
c2.resume();
//c1 用join將主執行緒掛起,自己先執行完再執行主執行緒.也就是保證自己必須先執行完成
//System.out.println("c1 將要執行 join");
c1.join();
System.out.println(Thread.currentThread().getName() + " 結束工作...after c1");
//c2 執行完
c2.join();
System.out.println(Thread.currentThread().getName() + " 結束工作...after c2");
}
/**
* 錯誤的 結果是:
* Thread in : T1
* resume by : T1
* Thread in : T2
* main 結束工作...after c1
* 並且程式一直掛起,無法結束.
* 列印執行緒資訊可以發現
* "[email protected]" prio=5 tid=0xd nid=NA runnable
* java.lang.Thread.State: RUNNABLE
* at java.lang.Thread.suspend0(Thread.java:-1)
* at java.lang.Thread.suspend(Thread.java:1029)
* at com.iboray.javacore.Thread.T2.SuspendExample$ChangeObj.run(SuspendExample.java:31)
* - locked <0x1b3> (a java.lang.Object)
*
* "[email protected]" prio=5 tid=0x1 nid=NA waiting
* java.lang.Thread.State: WAITING
* at java.lang.Object.wait(Object.java:-1)
* at java.lang.Thread.join(Thread.java:1245)
* at java.lang.Thread.join(Thread.java:1319)
* at com.iboray.javacore.Thread.T2.SuspendExample.main(SuspendExample.java:75)
*
* 正確的 結果是:
* Thread in : T1
* resume by : T1
* Thread in : T2
* main 結束工作...after c1
* resume by : T2
* main 結束工作...after c2
*/
}
示例2
通過wait和notify方式實現suspend和resume效果。這種方式類似於我們自己實現stop那樣
public class Suspend1Example {
public static Object u = new Object();
public static void main(String[] a) throws InterruptedException {
ChangeObj c = new ChangeObj();
ReadObj r = new ReadObj();
c.start();
r.start();
Thread.sleep(1000);
c.suspendMe();
System.out.println(" suspend ChangeObj 3s... ");
Thread.sleep(3000);
c.resumeMe();
/**
* 執行結果
* 剛開始ChangeObj與ReadObj交叉執行
in ChangeObj...
in ChangeObj...
in ReadObj...
in ChangeObj...
in ReadObj...
suspend ChangeObj 3s... 主執行緒執行c.suspendMe()
in ChangeObj... ...
in ChangeObj...suspend ChangeObj進入WAIT狀態
in ReadObj... ReadObj獨自執行
in ReadObj... ...
in ReadObj... ...
in ReadObj... ...
in ReadObj... ...
in ReadObj... ...
in ChangeObj...resume ChangeObj進入RUNNABLE狀態
in ChangeObj... 隨後ChangeObj與ReadObj又開始交叉執行
in ChangeObj...
in ReadObj...
in ChangeObj...
in ReadObj...
*/
}
public static class ChangeObj extends Thread{
//自定義掛起標識
volatile boolean suspend = false;
//設定掛起標識
public void suspendMe(){
this.suspend = true;
}
//模擬繼續執行
public void resumeMe(){
//設定掛起標識為 false 不掛起.
this.suspend = false;
//拿到當前物件鎖
synchronized (this){
//喚醒this物件等待佇列中的某一個執行緒.這裡單指當前這個
this.notify();
}
}
@Override
public void run() {
while (true){
//拿到當前物件的鎖,為什麼這裡同步的鎖一個是this一個是u呢?
//因為this鎖的作用是當前類
synchronized (this){
//如果設定了掛起為true
if (suspend){
try {
//讓當前物件加入this物件的等待佇列.同時可以釋放當前物件的鎖.
System.out.println(" in ChangeObj...suspend ");
this.wait();
System.out.println(" in ChangeObj...resume ");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//拿到指定例項物件的鎖
synchronized (u){
//執行業務
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(" in ChangeObj... ");
}
Thread.yield();
}
}
}
public static class ReadObj extends Thread{
@Override
public void run() {
while (true){
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
//拿到指定例項物件的鎖
synchronized (u){
//執行業務
System.out.println(" in ReadObj... ");
}
Thread.yield();
}
}
}
}
等待執行緒結束(join)和謙讓(yield)
一個執行緒的輸入可能依賴於另一或者多個執行緒的輸出,此時,這個執行緒就需要等待依賴執行緒執行完畢,才能繼續執行,JDK提供了join操作來實現這個功能。方法簽名:
//無限等待,它會一直阻塞當前執行緒,知道目標執行緒執行完畢
public final void join() throws InterruptedException
//包含最大等待時機,如果超過給定時間目標執行緒還未執行完成,當前執行緒會跳出阻塞 繼續執行
public final synchronized void join(long mills) throws InterruptedException
join的本質是讓呼叫執行緒wait()在當前執行緒物件例項上。當執行完成後,被等待的執行緒會在退出前呼叫notifyAll()通知所有的等待執行緒繼續執行。因此,需要注意,不要在應用程式中,在Thread上使用類似wait()或者notify()等方法,因為這很有可能影響系統API的工作,或者被系統API鎖影響
yield是一個靜態方法,一旦執行,它會使當前執行緒讓出CPU,然後繼續加入爭搶CPU的執行緒中。
volatile與JMM
當我們使用volatile來修飾變數,就等於告訴虛擬機器這個變數極有可能被某些程式或者執行緒修改。為了確保這個變數修改後,應用程式範圍內的所有執行緒都能夠看到。虛擬機器就必須採用一些特殊的手段保證這個變數的可見性。這樣就可以解決之前咱們在32位虛擬機器上用多執行緒修改long 的值了。
volatile並不代表鎖,他無法保證一些符合操作的原子性。他只能保證一個執行緒修改了資料後,其他執行緒能夠看到這個改動,但當多個執行緒同時修改某一個數據時,卻依然會產生衝突。他只能保證單個變數的完整性和可見性。保證原子性還的靠類似synchronized方式去解決。
執行緒組
如果執行緒數量很多,而且功能分配比較明確,就可以將相同的執行緒放置在一個執行緒組裡面。
public class ThreadGroupName implements Runnable{
public static void main(String[] a){
ThreadGroup threadGroupName = new ThreadGroup("printGroup");
Thread t1 = new Thread(threadGroupName,new ThreadGroupName(),"T1");
Thread t2 = new Thread(threadGroupName,new ThreadGroupName(),"T2");
t1.start();
t2.start();
//由於執行緒是動態的,activeCount()是一個估計值
System.out.println("printGroup執行緒組 活動執行緒數 : " + threadGroupName.activeCount());
//list()可以列印這個執行緒組中所有執行緒的資訊
threadGroupName.list();
//threadGroupName.stop(); 不推薦使用,和單個執行緒stop暴露的問題是一樣的。
/**
* printGroup執行緒組 活動執行緒數 : 2
* This is printGroup : T1
* java.lang.ThreadGroup[name=printGroup,maxpri=10]
* Thread[T1,5,printGroup]
* Thread[T2,5,printGroup]
* */
}
@Override
public void run() {
String groupName =
Thread.currentThread().getThreadGroup().getName() + " : "
+ Thread.currentThread().getName();
while (true){
System.out.println(" This is " + groupName);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread.yield();
}
}
}
守護執