?本節內容:
目錄
執行緒的狀態
wait/notify/notifyAll/sleep方法的介紹
如何正確停止執行緒
有哪些實作生產者消費者的方法
執行緒的狀態
執行緒一共有六種狀態,分別是New(新建)、Runnable(可運行)、Blocked(阻塞)、Waiting(等待)、Timed WaitIng(計時等待)、Terminated(終結)
狀態流轉圖

NEW(新建)
當我們new一個新執行緒的時候,如果還未呼叫start()方法,則該執行緒的狀態就是NEW,而一旦呼叫了start()方法,它就會從NEW變成Runnable
Runnable(可運行)
java中的可運行狀態分為兩種,一種是可運行,一種是運行中,如果當前執行緒呼叫了start()方法之后,還未獲取CPU時間片,此時該執行緒處于可運行狀態,等待被分配CPU資源,如果獲得CPU資源后,該執行緒就是運行狀態,
Blocked(阻塞)
java中的阻塞也分三種狀態:Blocked(被阻塞)、Waiting(等待)、Timed Waiting(計時等待),這三種狀態統稱為阻塞狀態,
Blocked狀態僅僅針對synchronized monitor鎖,如果獲取的鎖是ReentrantLock等鎖時,執行緒沒有搶到鎖就會進入Waiting狀態,因為本質上它執行的是LockSupport.park()方法,所以會進入Waiting方法,同樣Object.wait()、Thread.join()也會讓執行緒進入waiting狀態,Blocked和Waiting不同的是blocked等待其他執行緒釋放monitor鎖,而Waiting則是等待某個條件,類似join執行緒執行完畢或者notify()\notifyAll(),
上圖中可以看出處于Waiting、Time Waiting的執行緒呼叫notify()或者notifyAll()方法后,并不會進入Runnable狀態而是進入Blocked狀態,因為喚醒處于Waiting、Time Waiting狀態的執行緒的執行緒在呼叫notify()或者notifyAll()時候,必須持有該monitor鎖,所以處于Waiting、Time Waiting狀態的執行緒被喚醒后,就會進入Blocked狀態,直到執行了notify()\notifyAll()的執行緒釋放了鎖,被喚醒的執行緒才可以去搶奪這把鎖,如果搶到了就從Blocked狀態轉換到Runnable狀態
-
Blocked狀態(被阻塞):從結合圖中可以看出從Runnable狀態進入Blocked狀態只有進入synchronized保護的代碼時,沒有獲取到鎖monitor鎖,就會處于Blocked狀態
-
Time Waiting(計時等待):Time Waiting和Waiting狀態的區別是有沒有時間的限制,一下情況會進入Time Waiting:
-
設定了時間引數的Thread.sleep(long millis)
-
設定了時間引數的Object.wait(long timeout)
-
設定了時間引數的Thread.join(long millis)
-
設定了時間引數的LockSupport.parkNanos(long millis)和LockSupport.parkUntil(long deadline)
-
-
Waiting狀態(等待):執行緒進入Waiting狀態有三種情況,分別是:
-
沒有設定Timeout的Object.wait()方法
-
沒有設定Timeout的Thread.join()方法
-
LockSupport.park()方法
-
Terminated(終結)
進入這個狀態的執行緒分兩種情況:
-
-
run()方法執行完畢,正常退出
-
發生例外,終止了run()方法,
-
wait/notify/notifyAll方法的使用
首先wait方法必須在sychronized保護的同步代碼中使用,在wait方法的原始碼注釋中就有說:
在使用wait方法是必須把wait方法寫在synchronized保護的while代碼中,并且始終判斷執行條件是否滿足,如果滿足就繼續往下執行,不滿足就執行wait方法,而且執行wait方法前,必須先持有物件的synchronized鎖.
上面主要是兩點:
-
wait方法要在synchronized同步代碼中呼叫.
-
wait方法應該總是被呼叫在一個回圈中
我們先分析第一點,結合以下場景分析為什么要這么設計
public class TestDemo {private ArrayBlockingQueue<String> storage = new ArrayBlockingQueue(8);?public void add(String data){storage.add(data);notify();}?public String remove() throws InterruptedException {//wait不用synchronized關鍵字保護,直接呼叫,while (storage.isEmpty()){wait();}return storage.remove();}}
上述代碼是一個簡單的基于ArrayBlockingQueue實作的生產者、消費者模式,生產者呼叫add(String data)方法向storage中添加資料,消費者呼叫remove()方法從storage中消費資料.
代碼中我們可以看到如果wait方法的呼叫沒有用synchronized保護起來,那么就可能發生一下場景情況:
-
消費者執行緒呼叫remove()方法判斷storage是否為空,如果是就呼叫wait方法,消費者執行緒進入等待,但是這就可能發生消費者執行緒呼叫完storage.isEmpty()方法后就被調度器暫停了,然后還沒來得及執行wait方法.
-
此時生產者執行緒開始運行,開始執行了add(data)方法,成功的添加了data資料并且執行了notify()方法,但是因為之前的消費者還沒有執行wait方法,所以此時沒有執行緒被喚醒.
-
生產者執行完畢后,剛才被調度器暫停的消費者再回來執行wait方法,并且進入了等待,此時storage中已經有資料了.
以上的情況就是執行緒不安全的,因為wait方法的呼叫錯過了notify方法的喚醒,導致應該被喚醒的執行緒無法收到notify方法的喚醒.
正是因為wait方法的呼叫沒有被synchronized關鍵字保護,所以他和while判斷不是原子操作,所以就會出現執行緒安全問題.
我們把以上代碼改成如下,就實作了執行緒安全
public class TestDemo {private ArrayBlockingQueue<String> storage = new ArrayBlockingQueue(8);?public void add(String data){synchronized (this){storage.add(data);notify();}}public String remove() throws InterruptedException {synchronized (this){while (storage.isEmpty()){wait();}return storage.remove();}}}
我們再來分析第二點wait方法應該總是被呼叫在一個回圈中?
之所以將wait方法放到回圈中是為了防止執行緒“虛假喚醒“(spurious wakeup),執行緒可能在沒有被notify/notyfiAll,也沒有被中斷或者超時的情況下被喚醒,雖然這種概率發生非常小,但是為了保證發生虛假喚醒的正確性,所以需要采用回圈結構,這樣即便執行緒被虛假喚醒了,也會再次檢查while的條件是否滿足,不滿足就呼叫wait方法等待.
為什么wait/notify/notifyAll被定義在Object類中
java中每個物件都是一個內置鎖,都持有一把稱為monitor監視器的鎖,這就要求在物件頭中有一個用來保存鎖資訊的位置.這個鎖是物件級別的而非執行緒級別的,wait/notify/notifyAll也都是鎖級別的操作,它們的鎖屬于物件,所以把它們定義在Object中最合適.
wait/notify和sleep方法的異同
相同點:
-
它們都可以讓執行緒阻塞
-
它們都可以回應interrupt中斷:在等待程序中如果收到中斷信號,都可以進行回應并拋出InterruptedException例外
不同點:
-
wait方法必須在synchronized同步代碼中呼叫,sleep方法沒有這個要求
-
呼叫sleep不會釋放monitor鎖,呼叫wait方法就釋放monitor鎖
-
sleep要求等待一段時間后會自動恢復,但是wait方法沒有設定超時時間的話會一直等待,直到被中斷或者被喚醒,否則不能主動恢復
-
wait/notify是Object方法,sleep是Thread的方法
如何正確停止執行緒
正確的停止執行緒方式是通過使用interrupt方法,interrupt方法僅僅起到了通知需要被中斷的執行緒的作用,被中斷的執行緒有完全的自主權,它可以立刻停止,也可以執行一段時間再停止,或者壓根不停止.這是因為java希望程式之間能互相通知、協作的完成任務.
interrupt()方法的使用
public class InterruptDemo implements Runnable{?public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(new InterruptDemo());thread.start();Thread.sleep(5);thread.interrupt();}?@Overridepublic void run() {int i =0;while (!Thread.currentThread().isInterrupted() && i<1000){System.out.println(i++);}}}
上圖中通過回圈列印0~999,但是實際運行并不會列印到999,因為在執行緒列印到999之前,我們對執行緒呼叫了interrupt方法使其中斷了,然后根據while中的判斷條件,方法提前終止,運行結果如下:

其中如果是通過sleep、wait方法使執行緒陷入休眠,處于休眠期間的執行緒如果被中斷是可以感受到中斷信號的,并且會拋出一個InterruptException例外,同時清除中斷信號,將中斷標記位設定為false.
有哪些實作生產者消費者的方法
生產者消費者模式是程式設計中常見的一種設計模式,我們通過下圖來理解生產者消費者模式:
使用BolckingQueue實作生產者消費者模式
通過利用阻塞佇列ArrayBlockingQueue實作一個簡單的生產者消費者模式,創建兩個執行緒用來生產物件,兩個執行緒用來消費物件,如果ArrayBlockingQueue滿了,那么生產者就會阻塞,如果ArrayBlockingQueue為空,那么消費者執行緒就會阻塞.執行緒的阻塞和喚醒都是通過ArrayBlockingQueue來完成的.
public void MyBlockingQueue1(){BlockingQueue<Object> queue=new ArrayBlockingQueue<>(10);Runnable producer = () ->{while (true){try {queue.put(new Object());} catch (InterruptedException e) {e.printStackTrace();}}};new Thread(producer).start();new Thread(producer).start();?Runnable consumer = () ->{while (true){try {queue.take();} catch (InterruptedException e) {e.printStackTrace();}}};new Thread(consumer).start();new Thread(consumer).start();}
使用Condition實作生產者消費者模式
如下代碼其實也是類似ArrayBlockingQueue內部的實作原理.
如下代碼所示,定義了一個佇列容量是16的的queue,用來存放資料,定義一個ReentrantLock型別的鎖,并在Lock鎖的基礎上創建了兩個Condition,一個是notEmpty一個是notFull,分別代表佇列沒有空和沒有滿的條件,然后就是put和take方法.
put方法中,因為是多執行緒訪問環境,所以先上鎖,然后在while條件中判斷queue中是否已經滿了,如果滿了,則呼叫notFull的await()方法阻塞生產者并釋放Lock鎖,如果沒有滿則往佇列中放入資料,并且呼叫notEmpty.singleAll()方法喚醒所有的消費者執行緒,最后在finally中釋放鎖.
同理take方法和put方法類似,同樣是先上鎖,在判斷while條件是否滿足,然后執行對應的操作,最后在finally中釋放鎖.
public class MyBlockingQueue2 {private Queue queue;private int max;private ReentrantLock lock=new ReentrantLock();private Condition notEmpty = lock.newCondition();private Condition notFull =lock.newCondition();?public MyBlockingQueue2(int size){this.max =size;queue = new LinkedList();}?public void put(Object o) throws InterruptedException {lock.lock();try {while (queue.size() == max) {notFull.await();}queue.add(o);//喚醒所有的消費者notEmpty.signalAll();} finally {lock.unlock();}}?public Object take() throws InterruptedException{lock.lock();try {//這里不能改用if判斷,因為生產者喚醒了所有的消費者,//消費者喚醒后,必須在進行一次條件判斷while (queue.size() == 0) {notEmpty.await();}Object remove = queue.remove();//喚醒所有的生產者notFull.signalAll();return remove;}finally {lock.unlock();}}}
使用wait/notify實作生產者消費者模式
如下代碼所示,利用wait/notify實作生產者消費者模式主要是在put和take方法上加了synchronized鎖,并且在各自的while方法中進行條件判斷
public class MyBlockingQueue3 {private int max;private Queue<Object> queue;?public MyBlockingQueue3(int size){this.max =size;this.queue=new LinkedList<>();}public synchronized void put(Object o) throws InterruptedException {while(queue.size() == max){wait();}queue.add(o);notifyAll();}?public synchronized Object take() throws InterruptedException {while (queue.size() == 0){wait();}Object remove = queue.remove();notifyAll();return remove;}}
以上就是三種實作生產者消費者模式的方式,第一種比較簡單直接利用ArrayBlockingQueue內部的特征完成生產者消費者模式的實作場景,第二種是第一種背后的實作原理,第三種利用synchronzied實作.
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/263402.html
標籤:其他
