Java語法進階10-多執行緒
多執行緒
併發與並行、程序,執行緒排程自行百度
執行緒(thread):是一個程序中的其中一條執行路徑,CPU排程的最基本排程的單位。同一個程序中執行緒可以共享一些記憶體(堆、方法區),每一個執行緒又有自己的獨立空間(棧、程式計數器)。因為執行緒之間有共享的記憶體,在實現資料共享方面,比較方便,但是又因為共享資料的問題,會有執行緒安全問題。
當執行Java程式時,其實已經有一個執行緒了,那就是main執行緒。
Thread類
所有的執行緒物件都必須是Thread類或其子類的例項,Java中通過繼承Thread類來建立並啟動多執行緒的步驟如下:
-
定義Thread類的子類,並重寫該類的run()方法,該run()方法的方法體就代表了執行緒需要完成的任務,因此把run()方法稱為執行緒執行體。
-
建立Thread子類的例項,即建立了執行緒物件
-
呼叫執行緒物件的start()方法來啟動該執行緒
Runnable介面
我們還可以實現Runnable介面,重寫run()方法,然後再通過Thread類的物件代理啟動和執行我們的執行緒體run()方法。步驟如下:
-
定義Runnable介面的實現類,並重寫該介面的run()方法,該run()方法的方法體同樣是該執行緒的執行緒執行體。
-
建立Runnable實現類的例項,並以此例項作為Thread的target來建立Thread物件,該Thread物件才是真正 的執行緒物件。
-
呼叫執行緒物件的start()方法來啟動執行緒。
例: public class MyRunnable implements Runnable //定義實現執行緒類
MyRunnable mr = new MyRunnable(); //建立執行緒物件
Thread t = new Thread(mr); //通過Thread類的例項,啟動執行緒
t.start();
實際上所有的多執行緒程式碼都是通過執行Thread的start()方法來執行的。因此,不管是繼承Thread類還是實現 Runnable介面來實現多執行緒,最終還是通過Thread的物件的API來控制執行緒的,熟悉Thread類的API是進行多執行緒程式設計的基礎。
tips:Runnable物件僅僅作為Thread物件的target,Runnable實現類裡包含的run()方法僅作為執行緒執行體。 而實際的執行緒物件依然是Thread例項,只是該Thread執行緒負責執行其target的run()方法。
兩種方式的區別
1、繼承的方式有單繼承的限制,實現的方式可以多實現
2、啟動方式不同
3、繼承:在實現共享資料時,可能需要靜態的
實現:只要共享同一個Runnable實現類的物件即可。
4、繼承:選擇鎖時this可能不能用,
實現:選擇鎖時this可以用。
匿名內部類物件來實現執行緒的建立和啟動
new Thread("新的執行緒!"){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+":正在執行!"+i);
}
}
}.start();
構造方法
public Thread() :分配一個新的執行緒物件。
public Thread(String name) :分配一個指定名字的新的執行緒物件。
public Thread(Runnable target) :分配一個帶有指定目標新的執行緒物件。
public Thread(Runnable target,String name) :分配一個帶有指定目標新的執行緒物件並指定名字。
執行緒常用方法
volatile:修飾變數
變數不一定在什麼時候值就會被修改了,為了總是得到最新的值,volatile修飾之後那麼每次都從主存中去取值,不會在暫存器中快取它的值。
守護執行緒
守護執行緒有個特點,就是如果所有非守護執行緒都死亡,那麼守護執行緒自動死亡。
呼叫setDaemon(true)方法可將指定執行緒設定為守護執行緒。必須線上程啟動之前設定,否則會報IllegalThreadStateException異常。
呼叫isDaemon()可以判斷執行緒是否是守護執行緒。
執行緒安全
執行緒安全問題的判斷
1、是否有多個執行緒
2、這多個執行緒是否使用共享資料
3、這些執行緒在使用共享資料時,是否有寫有讀操作
同步程式碼塊
synchronized 關鍵字可以用於方法中的某個區塊中,表示只對這個區塊的資源實行互斥訪問。 格式:
synchronized(同步鎖){
需要同步操作的程式碼
}
同步鎖必須是物件
鎖物件可以是任意型別。
多個執行緒物件必須使用同一把鎖。 注意:在任何時候,最多允許一個執行緒擁有同步鎖,誰拿到鎖就進入程式碼塊,其他的執行緒只能在外等著(BLOCKED)。
同步方法
使用synchronized修飾的方法,就叫做同步方法,保證持有鎖執行緒執行該方法的時候,其他執行緒只能在方法外等著
【其他修飾符】 synchronized 返回值型別 方法名(【形參列表】)【throws 異常列表】{
//可能會產生執行緒安全問題的程式碼
}
鎖物件不能由我們自己選,它是預設的:
(1)靜態方法:鎖物件是當前類的Class物件
(2)非靜態方式:this
執行緒間通訊
當“資料緩衝區”滿的時候,“生產者”需要wait,等著被喚醒;
當“資料緩衝區”空的時候,“消費者”需要wait,等著被喚醒。
在一個執行緒滿足某個條件時,就進入等待狀態(wait()/wait(time)), 等待其他執行緒執行完他們的指定程式碼過後再將其喚醒(notify());或可以指定wait的時間,等時間到了自動喚醒;在有多個執行緒進行等待時,如果需要,可以使用 notifyAll()來喚醒所有的等待執行緒。
-
wait:執行緒不再活動,不再參與排程,進入 wait set 中,因此不會浪費 CPU 資源,也不會去競爭鎖了,這時的執行緒狀態即是 WAITING或TIMED_WAITING。它還要等著別的執行緒執行一個特別的動作,也即是“通知(notify)”或者等待時間到,在這個物件上等待的執行緒從wait set 中釋放出來,重新進入到排程佇列(ready queue)中
-
notify:則選取所通知物件的 wait set 中的一個執行緒釋放;
-
notifyAll:則釋放所通知物件的 wait set 上的全部執行緒。
注意:
被通知執行緒被喚醒後也不一定能立即恢復執行,因為它當初中斷的地方是在同步塊內,而此刻它已經不持有鎖,所以她需要再次嘗試去獲取鎖(很可能面臨其它執行緒的競爭),成功後才能在當初呼叫 wait 方法之後的地方恢復執行。
總結如下:
-
如果能獲取鎖,執行緒就從 WAITING 狀態變成 RUNNABLE(可執行) 狀態;
-
否則,執行緒就從 WAITING 狀態又變成 BLOCKED(等待鎖) 狀態
呼叫wait和notify方法需要注意的細節
-
wait方法與notify方法必須要由同一個鎖物件呼叫。因為:對應的鎖物件可以通過notify喚醒使用同一個鎖物件呼叫的wait方法後的執行緒。
-
wait方法與notify方法是屬於Object類的方法的。因為:鎖物件可以是任意物件,而任意物件的所屬類都是繼承了Object類的。
-
wait方法與notify方法必須要在同步程式碼塊或者是同步函式中使用。因為:必須要通過鎖物件呼叫這2個方法。
等待喚醒機制可以解決經典的“生產者與消費者”的問題
要解決該問題,就必須讓生產者執行緒在緩衝區滿時等待(wait),暫停進入阻塞狀態,等到下次消費者消耗了緩衝區中的資料的時候,通知(notify)正在等待的執行緒恢復到就緒狀態,重新開始往緩衝區新增資料。反之亦然
執行緒生命週期
一、站線上程的角度上:5種
1、新建:建立了執行緒物件,還未start
2、就緒:已啟動,並且可被CPU排程
3、執行:正在被排程
4、阻塞:遇到了:sleep(),wait(),wait(time),其它執行緒的join(),join(time),suspend(),鎖被其他執行緒佔用等
解除阻塞回到就緒狀態:sleep()時間,notify(),wait的時間到,加塞的執行緒結束,加塞的時間到,resume(),其他佔用鎖的執行緒釋放了鎖等。
5、死亡:run()正常結束,遇到了未處理的異常或錯誤,stop()
注意:
程式只能對新建狀態的執行緒呼叫start(),並且只能呼叫一次,如果對非新建狀態的執行緒,如已啟動的執行緒或已死亡的執行緒呼叫start()都會報錯IllegalThreadStateException異常。
二、站在程式碼的角度上6種
在java.lang.Thread.State的列舉類中這樣定義
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
1、新建NEW:建立了執行緒物件,還未start
2、可執行RUNNABLE:可以被CPU排程,或者正在被排程
3、阻塞BLOCKED:等待鎖
4、等待WAITING:wait(),join()等沒有設定時間的,必須等notify(),或加塞的執行緒結束才能恢復
5、有時間等待TIMED_WAITING:sleep(time),wait(time),join(time)等有時間的阻塞,等時間到了恢復,或被interrupt也會恢復
6、終止TERMINATED:run()正常結束,遇到了未處理的異常或錯誤,stop()
釋放鎖操作與死鎖
任何執行緒進入同步程式碼塊、同步方法之前,必須先獲得對同步監視器的鎖定,那麼何時會釋放對同步監視器的鎖定呢?
1、釋放鎖的操作
當前執行緒的同步方法、同步程式碼塊執行結束。
當前執行緒在同步程式碼塊、同步方法中出現了未處理的Error或Exception,導致當前執行緒異常結束。
當前執行緒在同步程式碼塊、同步方法中執行了鎖物件的wait()方法,當前執行緒被掛起,並釋放鎖。
2、不會釋放鎖的操作
執行緒執行同步程式碼塊或同步方法時,程式呼叫Thread.sleep()、Thread.yield()方法暫停當前執行緒的執行。
執行緒執行同步程式碼塊時,其他執行緒呼叫了該執行緒的suspend()方法將該執行緒掛起,該執行緒不會釋放鎖(同步監視器)。應儘量避免使用suspend()和resume()這樣的過時來控制執行緒。
3、死鎖
不同的執行緒分別鎖住對方需要的同步監視器物件不釋放,都在等待對方先放棄時就形成了執行緒的死鎖。一旦出現死鎖,整個程式既不會發生異常,也不會給出任何提示,只是所有執行緒處於阻塞狀態,無法繼續。
&n