本節(jié)內(nèi)容:
線程的狀態(tài)
wait/notify/notifyAll/sleep方法的介紹
如何正確停止線程
有哪些實現(xiàn)生產(chǎn)者消費者的方法
<span id="jump1">線程的狀態(tài)/span>
線程一共有六種狀態(tài)铺峭,分別是New(新建)墓怀、Runnable(可運行)、Blocked(阻塞)卫键、Waiting(等待)傀履、Timed WaitIng(計時等待)、Terminated(終結(jié))
狀態(tài)流轉(zhuǎn)圖
NEW(新建)
當我們new一個新線程的時候莉炉,如果還未調(diào)用start()方法钓账,則該線程的狀態(tài)就是NEW,而一旦調(diào)用了start()方法呢袱,它就會從NEW變成Runnable
Runnable(可運行)
java中的可運行狀態(tài)分為兩種官扣,一種是可運行,一種是運行中羞福,如果當前線程調(diào)用了start()方法之后惕蹄,還未獲取CPU時間片,此時該線程處于可運行狀態(tài),等待被分配CPU資源卖陵,如果獲得CPU資源后遭顶,該線程就是運行狀態(tài)。
Blocked(阻塞)
java中的阻塞也分三種狀態(tài):Blocked(被阻塞)泪蔫、Waiting(等待)棒旗、Timed Waiting(計時等待),這三種狀態(tài)統(tǒng)稱為阻塞狀態(tài)。
Blocked狀態(tài)(被阻塞):從結(jié)合圖中可以看出從Runnable狀態(tài)進入Blocked狀態(tài)只有進入synchronized保護的代碼時撩荣,沒有獲取到鎖monitor鎖铣揉,就會處于Blocked狀態(tài)
Time Waiting(計時等待):Time Waiting和Waiting狀態(tài)的區(qū)別是有沒有時間的限制,一下情況會進入Time Waiting:
設(shè)置了時間參數(shù)的Thread.sleep(long millis)
設(shè)置了時間參數(shù)的Object.wait(long timeout)
設(shè)置了時間參數(shù)的Thread.join(long millis)
設(shè)置了時間參數(shù)的LockSupport.parkNanos(long millis)和LockSupport.parkUntil(long deadline)
- Waiting狀態(tài)(等待):線程進入Waiting狀態(tài)有三種情況餐曹,分別是:
沒有設(shè)置Timeout的Object.wait()方法
沒有設(shè)置Timeout的Thread.join()方法
LockSupport.park()方法
Blocked狀態(tài)僅僅針對synchronized monitor鎖逛拱,如果獲取的鎖是ReentrantLock等鎖時,線程沒有搶到鎖就會進入Waiting狀態(tài)台猴,因為本質(zhì)上它執(zhí)行的是LockSupport.park()方法朽合,所以會進入Waiting方法,同樣Object.wait()饱狂、Thread.join()也會讓線程進入waiting狀態(tài)曹步。Blocked和Waiting不同的是blocked等待其他線程釋放monitor鎖,而Waiting則是等待某個條件休讳,類似join線程執(zhí)行完畢或者notify()\notifyAll()讲婚。
上圖中可以看出處于Waiting、Time Waiting的線程調(diào)用notify()或者notifyAll()方法后衍腥,并不會進入Runnable狀態(tài)而是進入Blocked狀態(tài)磺樱,因為喚醒處于Waiting纳猫、Time Waiting狀態(tài)的線程的線程在調(diào)用notify()或者notifyAll()時候婆咸,必須持有該monitor鎖,所以處于Waiting芜辕、Time Waiting狀態(tài)的線程被喚醒后尚骄,就會進入Blocked狀態(tài),直到執(zhí)行了notify()\notifyAll()的線程釋放了鎖侵续,被喚醒的線程才可以去搶奪這把鎖倔丈,如果搶到了就從Blocked狀態(tài)轉(zhuǎn)換到Runnable狀態(tài)
****Terminated(終結(jié))****
進入這個狀態(tài)的線程分兩種情況:
run()方法執(zhí)行完畢,正常退出
發(fā)生異常状蜗,終止了run()方法需五。
<span id="jump2">wait/notify/notifyAll方法的使用</span>
首先wait方法必須在sychronized保護的同步代碼中使用,在wait方法的源碼注釋中就有說:
在使用wait方法是必須把wait方法寫在synchronized保護的while代碼中,并且始終判斷執(zhí)行條件是否滿足,如果滿足就繼續(xù)往下執(zhí)行,不滿足就執(zhí)行wait方法,而且執(zhí)行wait方法前,必須先持有對象的synchronized鎖.
上面主要是兩點:
wait方法要在synchronized同步代碼中調(diào)用.
wait方法應(yīng)該總是被調(diào)用在一個循環(huán)中
我們先分析第一點,結(jié)合以下場景分析為什么要這么設(shè)計
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關(guān)鍵字保護,直接調(diào)用轧坎,
while (storage.isEmpty()){
wait();
}
return storage.remove();
}
}
上述代碼是一個簡單的基于ArrayBlockingQueue實現(xiàn)的生產(chǎn)者宏邮、消費者模式,生產(chǎn)者調(diào)用add(String data)方法向storage中添加數(shù)據(jù),消費者調(diào)用remove()方法從storage中消費數(shù)據(jù).
代碼中我們可以看到如果wait方法的調(diào)用沒有用synchronized保護起來,那么就可能發(fā)生一下場景情況:
消費者線程調(diào)用remove()方法判斷storage是否為空,如果是就調(diào)用wait方法,消費者線程進入等待,但是這就可能發(fā)生消費者線程調(diào)用完storage.isEmpty()方法后就被調(diào)度器暫停了,然后還沒來得及執(zhí)行wait方法.
此時生產(chǎn)者線程開始運行,開始執(zhí)行了add(data)方法,成功的添加了data數(shù)據(jù)并且執(zhí)行了notify()方法,但是因為之前的消費者還沒有執(zhí)行wait方法,所以此時沒有線程被喚醒.
生產(chǎn)者執(zhí)行完畢后,剛才被調(diào)度器暫停的消費者再回來執(zhí)行wait方法,并且進入了等待,此時storage中已經(jīng)有數(shù)據(jù)了.
以上的情況就是線程不安全的,因為wait方法的調(diào)用錯過了notify方法的喚醒,導(dǎo)致應(yīng)該被喚醒的線程無法收到notify方法的喚醒.
正是因為wait方法的調(diào)用沒有被synchronized關(guān)鍵字保護,所以他和while判斷不是原子操作,所以就會出現(xiàn)線程安全問題.
我們把以上代碼改成如下,就實現(xiàn)了線程安全
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方法應(yīng)該總是被調(diào)用在一個循環(huán)中?
之所以將wait方法放到循環(huán)中是為了防止線程“虛假喚醒“(spurious wakeup),線程可能在沒有被notify/notyfiAll,也沒有被中斷或者超時的情況下被喚醒,雖然這種概率發(fā)生非常小,但是為了保證發(fā)生虛假喚醒的正確性,所以需要采用循環(huán)結(jié)構(gòu),這樣即便線程被虛假喚醒了,也會再次檢查while的條件是否滿足,不滿足就調(diào)用wait方法等待.
為什么wait/notify/notifyAll被定義在Object類中
java中每個對象都是一個內(nèi)置鎖,都持有一把稱為monitor監(jiān)視器的鎖,這就要求在對象頭中有一個用來保存鎖信息的位置.這個鎖是對象級別的而非線程級別的,wait/notify/notifyAll也都是鎖級別的操作,它們的鎖屬于對象,所以把它們定義在Object中最合適.
wait/notify和sleep方法的異同
相同點:
它們都可以讓線程阻塞
它們都可以響應(yīng)interrupt中斷:在等待過程中如果收到中斷信號,都可以進行響應(yīng)并拋出InterruptedException異常
不同點:
wait方法必須在synchronized同步代碼中調(diào)用,sleep方法沒有這個要求
調(diào)用sleep不會釋放monitor鎖,調(diào)用wait方法就釋放monitor鎖
sleep要求等待一段時間后會自動恢復(fù),但是wait方法沒有設(shè)置超時時間的話會一直等待,直到被中斷或者被喚醒,否則不能主動恢復(fù)
wait/notify是Object方法,sleep是Thread的方法
<span id="jump3">如何正確停止線程</span>
正確的停止線程方式是通過使用interrupt方法,interrupt方法僅僅起到了通知需要被中斷的線程的作用,被中斷的線程有完全的自主權(quán),它可以立刻停止,也可以執(zhí)行一段時間再停止,或者壓根不停止.這是因為java希望程序之間能互相通知、協(xié)作的完成任務(wù).
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();
}
@Override
public void run() {
int i =0;
while (!Thread.currentThread().isInterrupted() && i<1000){
System.out.println(i++);
}
}
}
上圖中通過循環(huán)打印0~999,但是實際運行并不會打印到999,因為在線程打印到999之前,我們對線程調(diào)用了interrupt方法使其中斷了,然后根據(jù)while中的判斷條件,方法提前終止,運行結(jié)果如下:
其中如果是通過sleep、wait方法使線程陷入休眠,處于休眠期間的線程如果被中斷是可以感受到中斷信號的,并且會拋出一個InterruptException異常,同時清除中斷信號,將中斷標記位設(shè)置為false.
<span id="jump3">有哪些實現(xiàn)生產(chǎn)者消費者的方法</span>
生產(chǎn)者消費者模式是程序設(shè)計中常見的一種設(shè)計模式,我們通過下圖來理解生產(chǎn)者消費者模式:
使用BolckingQueue實現(xiàn)生產(chǎn)者消費者模式
通過利用阻塞隊列ArrayBlockingQueue實現(xiàn)一個簡單的生產(chǎn)者消費者模式,創(chuàng)建兩個線程用來生產(chǎn)對象,兩個線程用來消費對象,如果ArrayBlockingQueue滿了,那么生產(chǎn)者就會阻塞,如果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實現(xiàn)生產(chǎn)者消費者模式
如下代碼其實也是類似ArrayBlockingQueue內(nèi)部的實現(xiàn)原理.
如下代碼所示,定義了一個隊列容量是16的的queue,用來存放數(shù)據(jù),定義一個ReentrantLock類型的鎖,并在Lock鎖的基礎(chǔ)上創(chuàng)建了兩個Condition,一個是notEmpty一個是notFull,分別代表隊列沒有空和沒有滿的條件,然后就是put和take方法.
put方法中,因為是多線程訪問環(huán)境,所以先上鎖,然后在while條件中判斷queue中是否已經(jīng)滿了,如果滿了,則調(diào)用notFull的await()方法阻塞生產(chǎn)者并釋放Lock鎖,如果沒有滿則往隊列中放入數(shù)據(jù),并且調(diào)用notEmpty.singleAll()方法喚醒所有的消費者線程,最后在finally中釋放鎖.
同理take方法和put方法類似,同樣是先上鎖,在判斷while條件是否滿足,然后執(zhí)行對應(yīng)的操作,最后在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判斷,因為生產(chǎn)者喚醒了所有的消費者,
//消費者喚醒后,必須在進行一次條件判斷
while (queue.size() == 0) {
notEmpty.await();
}
Object remove = queue.remove();
//喚醒所有的生產(chǎn)者
notFull.signalAll();
return remove;
}finally {
lock.unlock();
}
}
}
使用wait/notify實現(xiàn)生產(chǎn)者消費者模式
如下代碼所示,利用wait/notify實現(xiàn)生產(chǎn)者消費者模式主要是在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;
}
}
以上就是三種實現(xiàn)生產(chǎn)者消費者模式的方式,第一種比較簡單直接利用ArrayBlockingQueue內(nèi)部的特征完成生產(chǎn)者消費者模式的實現(xiàn)場景,第二種是第一種背后的實現(xiàn)原理,第三種利用synchronzied實現(xiàn).