多線程基礎(chǔ)知識

本節(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)圖

image

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)的線程分兩種情況:

  1. run()方法執(zhí)行完畢,正常退出

  2. 發(fā)生異常状蜗,終止了run()方法需五。

<span id="jump2">wait/notify/notifyAll方法的使用</span>

首先wait方法必須在sychronized保護的同步代碼中使用,在wait方法的源碼注釋中就有說:

在使用wait方法是必須把wait方法寫在synchronized保護的while代碼中,并且始終判斷執(zhí)行條件是否滿足,如果滿足就繼續(xù)往下執(zhí)行,不滿足就執(zhí)行wait方法,而且執(zhí)行wait方法前,必須先持有對象的synchronized鎖.

上面主要是兩點:

  1. wait方法要在synchronized同步代碼中調(diào)用.

  2. 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ā)生一下場景情況:

  1. 消費者線程調(diào)用remove()方法判斷storage是否為空,如果是就調(diào)用wait方法,消費者線程進入等待,但是這就可能發(fā)生消費者線程調(diào)用完storage.isEmpty()方法后就被調(diào)度器暫停了,然后還沒來得及執(zhí)行wait方法.

  2. 此時生產(chǎn)者線程開始運行,開始執(zhí)行了add(data)方法,成功的添加了data數(shù)據(jù)并且執(zhí)行了notify()方法,但是因為之前的消費者還沒有執(zhí)行wait方法,所以此時沒有線程被喚醒.

  3. 生產(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方法的異同

相同點:

  1. 它們都可以讓線程阻塞

  2. 它們都可以響應(yīng)interrupt中斷:在等待過程中如果收到中斷信號,都可以進行響應(yīng)并拋出InterruptedException異常

不同點:

  1. wait方法必須在synchronized同步代碼中調(diào)用,sleep方法沒有這個要求

  2. 調(diào)用sleep不會釋放monitor鎖,調(diào)用wait方法就釋放monitor鎖

  3. sleep要求等待一段時間后會自動恢復(fù),但是wait方法沒有設(shè)置超時時間的話會一直等待,直到被中斷或者被喚醒,否則不能主動恢復(fù)

  4. 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é)果如下:

image

其中如果是通過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).

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蜜氨,一起剝皮案震驚了整個濱河市械筛,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌飒炎,老刑警劉巖埋哟,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異郎汪,居然都是意外死亡赤赊,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進店門煞赢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來砍鸠,“玉大人,你說我怎么就攤上這事耕驰∫瑁” “怎么了?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵朦肘,是天一觀的道長饭弓。 經(jīng)常有香客問我,道長媒抠,這世上最難降的妖魔是什么弟断? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮趴生,結(jié)果婚禮上阀趴,老公的妹妹穿的比我還像新娘。我一直安慰自己苍匆,他們只是感情好刘急,可當我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著浸踩,像睡著了一般叔汁。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上检碗,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天据块,我揣著相機與錄音,去河邊找鬼折剃。 笑死另假,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的怕犁。 我是一名探鬼主播边篮,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼开睡,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了苟耻?” 一聲冷哼從身側(cè)響起篇恒,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎凶杖,沒想到半個月后胁艰,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡智蝠,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年腾么,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片杈湾。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡解虱,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出漆撞,到底是詐尸還是另有隱情殴泰,我是刑警寧澤,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布浮驳,位于F島的核電站悍汛,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏至会。R本人自食惡果不足惜离咐,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望奉件。 院中可真熱鬧宵蛀,春花似錦、人聲如沸县貌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽窃这。三九已至瞳别,卻和暖如春征候,著一層夾襖步出監(jiān)牢的瞬間杭攻,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工疤坝, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留兆解,地道東北人。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓跑揉,卻偏偏與公主長得像锅睛,于是被迫代替她去往敵國和親埠巨。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 一现拒、線程基礎(chǔ) 1.1線程的實現(xiàn)方法 (1)繼承Thread 類(2)實現(xiàn)Runnable接口:Thread t1=...
    黑色叉腰魔頭閱讀 248評論 0 1
  • 創(chuàng)建辣垒、啟動、控制印蔬、多線程同步勋桶、線程池 進程和線程 進程:是處于運行過程的程序,有一定的獨立功能侥猬,是系統(tǒng)進行資源分配...
    長遠勿見閱讀 301評論 0 0
  • 什么是線程池例驹?為什么要使用線程池? 將線程池化退唠,需要運行任務(wù)時就從里面拿出來一個鹃锈,不需要了就放回去,不需要每次都n...
    閆回閱讀 400評論 0 1
  • 線程概述 線程與進程 進程 ?每個運行中的任務(wù)(通常是程序)就是一個進程瞧预。當一個程序進入內(nèi)存運行時屎债,即變成了一個進...
    閩越布衣閱讀 1,010評論 1 7
  • 一、線程基本概念 1. 線程的五種狀態(tài) 新建狀態(tài)(new): 線程對象被創(chuàng)建后垢油,就進入了新建狀態(tài)扔茅。例如,Threa...
    Lynn_R01612x2閱讀 437評論 0 1