多線程中的wait與join

本來是想把標題定為《Thread的wait與join》焊虏,后來想想不嚴謹沮翔,因為wait是Object的方法焰望,不是Thread獨有的骚亿,所以這里要注意一下。
關于wait()方法熊赖,在Object中有三個重載方法:

    public final native void wait(long timeout) throws InterruptedException;

    public final void wait(long timeout, int nanos) throws InterruptedException {
        if (timeout < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos > 0) {
            timeout++;
        }

        wait(timeout);
    }

    public final void wait() throws InterruptedException {
        wait(0);
    }

無論是哪個来屠,最終都是要調用本地方法 wait(long timeout) 。
開局一張圖:


image.png

今天主要就來講一下wait()震鹉,join()的關系吧俱笛。

wait

一個Object的方法,目的是將調用obj.wait()的線程置為waiting的狀態(tài)传趾,等待其他線程調用obj.notify()或者obj.notifyAll()來喚醒迎膜。最常見的就是生產者/消費者功能。

有一點注意的就是浆兰,wait/notify方法的調用必須處在該對象的鎖(Monitor)中磕仅,也即,在調用這些方法時首先需要獲得該對象的鎖簸呈。否則會拋出IllegalMonitorStateException異常榕订。

wait/notify的通俗解釋:
1.線程A首先獲取到obj的鎖,然后執(zhí)行了obj.wait()蜕便,這個方法就會是線程A暫時讓出對obj鎖的持有劫恒,并把線程A轉換waiting狀態(tài),同時加入鎖對象的等待隊列轿腺。
2.線程B獲取到了obj的鎖两嘴,然后執(zhí)行了obj.notify()丛楚,這個方法通知了鎖對象的等待隊列,使正在等待隊列中的線程A改為阻塞狀態(tài)憔辫,使A進入對obj鎖的競爭鸯檬。當然在執(zhí)行notify后并不會使線程A馬上獲取到鎖,因為線程B目前還在持有obj的鎖螺垢。
3.線程A獲取到obj鎖,繼續(xù)從wait()之后的代碼運行赖歌。

舉個例子:

public class ThreadTest {

    static final Object obj = new Object();  //對象鎖

    private static boolean flag = false;

    public static void main(String[] args) throws Exception {

        Thread consume = new Thread(new Consume(), "Consume");
        Thread produce = new Thread(new Produce(), "Produce");
        consume.start();
        Thread.sleep(1000);
        produce.start();

        try {
            produce.join();
            consume.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // 生產者線程
    static class Produce implements Runnable {

        @Override
        public void run() {

            synchronized (obj) {
                System.out.println("進入生產者線程");
                System.out.println("生產");
                try {
                    TimeUnit.MILLISECONDS.sleep(2000);  //模擬生產過程
                    flag = true;
                    obj.notify();  //通知消費者
                    TimeUnit.MILLISECONDS.sleep(1000);  //模擬其他耗時操作
                    System.out.println("退出生產者線程");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    //消費者線程
    static class Consume implements Runnable {

        @Override
        public void run() {
            synchronized (obj) {
                System.out.println("進入消費者線程");
                System.out.println("wait flag 1:" + flag);
                while (!flag) {  //判斷條件是否滿足枉圃,若不滿足則等待
                    try {
                        System.out.println("還沒生產,進入等待");
                        obj.wait();
                        System.out.println("結束等待");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("wait flag 2:" + flag);
                System.out.println("消費");
                System.out.println("退出消費者線程");
            }

        }
    }
}

結果:

進入消費者線程
wait flag 1:false
還沒生產庐冯,進入等待
進入生產者線程
生產 // 1秒后notify
退出生產者線程 //生產線程結束
結束等待
wait flag 2:true
消費
退出消費者線程

可以看到孽亲,生產線程在notify后,消費線程并沒有馬上繼續(xù)運行展父,原因就是上面提到的第二點返劲。

wait()方法有了一個大概的了解,下面看看join的原理栖茉。

join

join方法通常的解釋就是等待調用線程執(zhí)行完畢后篮绿,再繼續(xù)執(zhí)行當前線程。通常用在多線程的業(yè)務上吕漂,某個線程的運算需要另一個線程的結果時亲配,就可以使用join。

來個例子:

public class JoinMainDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new Runnable() {
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("thread 1");
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("thread 2");
            }
        });
        Thread thread3 = new Thread(new Runnable() {
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("thread 3");
            }
        });

        thread1.start();
        thread1.join();
        thread2.start();
        thread2.join();
        thread3.start();
        thread3.join();

        System.out.println("thread Main");
    }
}

情況1輸出:

thread 1
thread 2
thread 3
thread Main

流程:Main線程調用thread1join后惶凝,會等待join執(zhí)行完畢才會繼續(xù)運行吼虎,thread2,thread3都是這樣苍鲜。1思灰,2,3順序是固定的混滔。

我們改一下代碼順序

        thread1.start();
        thread3.start();
        thread2.start();
        thread1.join();
        thread2.join();
        thread3.join();

情況2輸出:

thread 3
thread 1
thread 2
thread Main

結果是線程1洒疚,2,3隨機順序遍坟,Main一定在最后拳亿。

去掉join:

        thread1.start();
        thread3.start();
        thread2.start();

情況3輸出:

thread Main
thread 3
thread 1
thread 2

這個就是普通的線程執(zhí)行結果。

我們來分析一下每種情況的原因愿伴。join的行為像不像被wait后自動釋放的過程肺魁?看一下join的實現:

 public final void join() throws InterruptedException {
        join(0);
    }
 public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

可見,線程A在調用obj.join方法時隔节,obj線程如果是alive狀態(tài)(線程開啟start鹅经,但未結束)寂呛,那么就執(zhí)行wait方法。同時看join(long m)方法是synchronized修飾的瘾晃,這是我們使用wait時需要先獲取鎖的前置條件贷痪。既然知道了join的內核是wait方法,通過對wait的了解蹦误,線程A此時是waiting的狀態(tài)劫拢,并進入了obj鎖的等待隊列排隊去。那么是誰在什么時候釋放了線程A呢强胰?

這時要了解一下Thread.exit()方法:

/**
* 這個方法由系統調用舱沧,當該線程完全退出前給它一個機會去釋放空間。
*/
 private void exit() {
        if (group != null) {
            group.threadTerminated(this);
            group = null;
        }
        /* Aggressively null out all reference fields: see bug 4006245 */
        target = null;
        /* Speed the release of some of these resources */
        threadLocals = null;
        inheritableThreadLocals = null;
        inheritedAccessControlContext = null;
        blocker = null;
        uncaughtExceptionHandler = null;
    }

這個方法由系統調用偶洋,當該線程完全退出前給它一個機會去釋放空間熟吏。再往下跟到threadTerminated(this)方法

    void threadTerminated(Thread t) {
        synchronized (this) {
            remove(t);

            if (nthreads == 0) {
                notifyAll();
            }
            if (daemon && (nthreads == 0) &&
                (nUnstartedThreads == 0) && (ngroups == 0))
            {
                destroy();
            }
        }
    }

這里有一個notifyAll()的方法,可見就是在這里是線程A得到了釋放繼續(xù)運行玄窝。

到這里就解釋通了牵寺,join的確是通過wait方法使調用線程變?yōu)榈却隣顟B(tài),再在被調用線程運行結束時通過系統調用exit方法啟動了notifyAll恩脂。

我們可以這么理解帽氓,把obj.join方法替換成obj.wait()方法,并且在obj線程運行結束后自動執(zhí)行notifyAll()方法东亦,這樣就可以用wait的思路來理解join的運行過程了杏节。

好了,現在回到上面例子的三種情況:
情況1: 可以發(fā)現典阵,thread2.start前有thread1.join奋渔,thread3.start前有thread2.join,這樣就能解釋thread1,2,3是按順序執(zhí)行的壮啊。thread1.join的時候嫉鲸,Main線程進入了thread1鎖對象的等待隊列,只有thread1運行完成后才會得到釋放歹啼。進而才會按順序開啟線程玄渗,thread2.start,thread3.start狸眼。

情況2:先將3個線程開啟藤树,再依次執(zhí)行join。join之前拓萌,三個線程已經都在運行岁钓,所以輸出的順序并沒有固定,只是會控制Main線程運行時間。Main的輸出肯定是在最后的屡限。當然我們可以把thread2的sleep時間調大一點品嚣,并且不執(zhí)行join再來運行看看結果,自己嘗試解釋一下钧大。

情況3:沒有join翰撑,各個線程獨自運行,互不影響啊央。

總結

1.wait的注意點: wait方法是Object的方法眶诈; wait/notify方法需要獲得對象鎖后執(zhí)行。
2.wait方法會把調用線程轉為等待waiting狀態(tài)瓜饥,釋放對象鎖册养,并進入對象鎖的等待隊列。
3.notify/notifyAll方法會喚醒對象鎖的等待隊列压固,使其中的線程進入阻塞blocking狀態(tài)搶占對象鎖。
4.調用notify/notifyAll后靠闭,在notify/notifyAll的前獲得的對象鎖得到釋放后帐我,等待隊列里的線程才有機會搶占鎖繼續(xù)執(zhí)行。
5.join方法的內核就是wait愧膀。在被調用對象的線程運行完畢后拦键,系統自動調 用被調用對象notifyAll方法。

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末檩淋,一起剝皮案震驚了整個濱河市芬为,隨后出現的幾起案子,更是在濱河造成了極大的恐慌蟀悦,老刑警劉巖媚朦,帶你破解...
    沈念sama閱讀 221,331評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異日戈,居然都是意外死亡询张,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 94,372評論 3 398
  • 文/潘曉璐 我一進店門浙炼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來份氧,“玉大人,你說我怎么就攤上這事弯屈∥现模” “怎么了?”我有些...
    開封第一講書人閱讀 167,755評論 0 360
  • 文/不壞的土叔 我叫張陵资厉,是天一觀的道長厅缺。 經常有香客問我,道長,這世上最難降的妖魔是什么店归? 我笑而不...
    開封第一講書人閱讀 59,528評論 1 296
  • 正文 為了忘掉前任阎抒,我火速辦了婚禮,結果婚禮上消痛,老公的妹妹穿的比我還像新娘且叁。我一直安慰自己,他們只是感情好秩伞,可當我...
    茶點故事閱讀 68,526評論 6 397
  • 文/花漫 我一把揭開白布逞带。 她就那樣靜靜地躺著,像睡著了一般纱新。 火紅的嫁衣襯著肌膚如雪展氓。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,166評論 1 308
  • 那天脸爱,我揣著相機與錄音遇汞,去河邊找鬼。 笑死簿废,一個胖子當著我的面吹牛空入,可吹牛的內容都是我干的。 我是一名探鬼主播族檬,決...
    沈念sama閱讀 40,768評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼歪赢,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了单料?” 一聲冷哼從身側響起埋凯,我...
    開封第一講書人閱讀 39,664評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎扫尖,沒想到半個月后白对,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 46,205評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡换怖,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,290評論 3 340
  • 正文 我和宋清朗相戀三年躏结,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片狰域。...
    茶點故事閱讀 40,435評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡媳拴,死狀恐怖,靈堂內的尸體忽然破棺而出兆览,到底是詐尸還是另有隱情屈溉,我是刑警寧澤,帶...
    沈念sama閱讀 36,126評論 5 349
  • 正文 年R本政府宣布抬探,位于F島的核電站子巾,受9級特大地震影響帆赢,放射性物質發(fā)生泄漏。R本人自食惡果不足惜线梗,卻給世界環(huán)境...
    茶點故事閱讀 41,804評論 3 333
  • 文/蒙蒙 一椰于、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧仪搔,春花似錦瘾婿、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,276評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至煮嫌,卻和暖如春笛谦,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背昌阿。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評論 1 272
  • 我被黑心中介騙來泰國打工饥脑, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人懦冰。 一個月前我還...
    沈念sama閱讀 48,818評論 3 376
  • 正文 我出身青樓好啰,卻偏偏與公主長得像,于是被迫代替她去往敵國和親儿奶。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,442評論 2 359

推薦閱讀更多精彩內容

  • 本文主要講了java中多線程的使用方法鳄抒、線程同步闯捎、線程數據傳遞、線程狀態(tài)及相應的一些線程函數用法许溅、概述等瓤鼻。 首先講...
    李欣陽閱讀 2,458評論 1 15
  • 林炳文Evankaka原創(chuàng)作品。轉載自http://blog.csdn.net/evankaka 本文主要講了ja...
    ccq_inori閱讀 656評論 0 4
  • Java多線程學習 [-] 一擴展javalangThread類 二實現javalangRunnable接口 三T...
    影馳閱讀 2,964評論 1 18
  • 一贤重、進程和線程 進程 進程就是一個執(zhí)行中的程序實例茬祷,每個進程都有自己獨立的一塊內存空間,一個進程中可以有多個線程并蝗。...
    阿敏其人閱讀 2,612評論 0 13
  • 此片文章主要總結的是Thread類及相關的基礎概念和API狈谊,首先需要厘清線程調度中的幾個基本概念: 一适滓、線程調度的...
    千淘萬漉閱讀 2,570評論 0 2