Java 并發(fā)編程 001 | 深入理解Object類的 wait 和 notify

概述

線程的狀態(tài)

Java 中線程中狀態(tài)可分為五種:New(新建狀態(tài))容贝,Runnable(就緒狀態(tài)),Running(運(yùn)行狀態(tài))季研,Blocked(阻塞狀態(tài))座每,Dead(死亡狀態(tài))

  • New:新建狀態(tài),當(dāng)線程創(chuàng)建完成時(shí)為新建狀態(tài)高诺,即new Thread(...)碌识,還沒有調(diào)用start方法時(shí),線程處于新建狀態(tài)虱而。

  • Runnable:就緒狀態(tài)筏餐,當(dāng)調(diào)用線程的的start方法后,線程進(jìn)入就緒狀態(tài)薛窥,等待CPU資源胖烛。處于就緒狀態(tài)的線程由Java運(yùn)行時(shí)系統(tǒng)的線程調(diào)度程序(thread scheduler)來調(diào)度眼姐。

  • Running:運(yùn)行狀態(tài),就緒狀態(tài)的線程獲取到CPU執(zhí)行權(quán)以后進(jìn)入運(yùn)行狀態(tài)佩番,開始執(zhí)行run方法众旗。

  • Blocked:阻塞狀態(tài),線程沒有執(zhí)行完趟畏,由于某種原因(如贡歧,I/O操作等)讓出CPU執(zhí)行權(quán),自身進(jìn)入阻塞狀態(tài)赋秀。

  • Dead:死亡狀態(tài)利朵,線程執(zhí)行完成或者執(zhí)行過程中出現(xiàn)異常,線程就會(huì)進(jìn)入死亡狀態(tài)猎莲。

這五種狀態(tài)之間的轉(zhuǎn)換關(guān)系如下圖所示

image

Java中是如何實(shí)現(xiàn)這幾種狀態(tài)的轉(zhuǎn)換的

通過 wait/notify/notifyAll 方法的使用

這三個(gè)方法都是在 Object 對(duì)象中定義的绍弟,本篇詳細(xì)講解

通過 sleep/yield/join 方法的使用

這三個(gè)方法都是在 Thread 對(duì)象中定義的,這里不做講解著洼,具體可看 深入理解Thread類 這篇文章

各個(gè)方法解析

以下是 Object 類的源碼

public class Object {
        public final native void notify();
  
    public final native void notifyAll();
  
    public final native void wait(long timeout) throws InterruptedException;
  
    public final void wait() throws InterruptedException {
        wait(0);
    }
}

wait樟遣、notify和notifyAll方法是Object類的final native方法。所以這些方法不能被子類重寫身笤,Object類是所有類的超類豹悬,因此在程序中有以下三種形式調(diào)用wait等方法。

wait();//方式1:
this.wait();//方式2:
super.wait();//方式3

void wait()

導(dǎo)致線程進(jìn)入等待狀態(tài)液荸,直到它被其他線程通過notify()或者notifyAll喚醒瞻佛。該方法只能在同步方法同步塊中調(diào)用。如果當(dāng)前線程不是鎖的持有者娇钱,該方法拋出一個(gè)IllegalMonitorStateException異常伤柄。

void wait(long millis) 和 void wait(long millis, int nanos)

導(dǎo)致線程進(jìn)入等待狀態(tài)直到它被通知或者經(jīng)過指定的時(shí)間。這些方法只能在同步方法同步塊中調(diào)用忍弛。如果當(dāng)前線程不是鎖的持有者响迂,該方法拋出一個(gè)IllegalMonitorStateException異常。

void notifyAll()

解除所有那些在該對(duì)象上調(diào)用wait方法的線程的阻塞狀態(tài)细疚。該方法只能在同步方法同步塊內(nèi)部調(diào)用。如果當(dāng)前線程不是鎖的持有者川梅,該方法拋出一個(gè)IllegalMonitorStateException異常疯兼。

void notify()

隨機(jī)選擇一個(gè)在該對(duì)象上調(diào)用wait方法的線程,解除其阻塞狀態(tài)贫途。該方法只能在同步方法同步塊內(nèi)部調(diào)用吧彪。如果當(dāng)前線程不是鎖的持有者,該方法拋出一個(gè)IllegalMonitorStateException異常丢早。

Object.wait()和Object.notify()和Object.notifyall()必須寫在synchronized方法內(nèi)部或者synchronized塊內(nèi)部姨裸,這是因?yàn)椋?strong>這幾個(gè)方法要求當(dāng)前正在運(yùn)行object.wait()方法的線程擁有object的對(duì)象鎖秧倾。即使你確實(shí)知道當(dāng)前上下文線程確實(shí)擁有了對(duì)象鎖,也不能將object.wait()這樣的語(yǔ)句寫在當(dāng)前上下文中傀缩。

從這三個(gè)方法的文字描述可以知道以下幾點(diǎn)信息:

1)wait()那先、notify()和notifyAll()方法是本地方法,并且為final方法赡艰,無法被重寫售淡。

2)調(diào)用某個(gè)對(duì)象的wait()方法能讓當(dāng)前線程阻塞,并且當(dāng)前線程必須擁有此對(duì)象的monitor(即鎖)

3)調(diào)用某個(gè)對(duì)象的notify()方法能夠喚醒一個(gè)正在等待這個(gè)對(duì)象的monitor的線程慷垮,如果有多個(gè)線程都在等待這個(gè)對(duì)象的monitor揖闸,則只能喚醒其中一個(gè)線程;

4)調(diào)用notifyAll()方法能夠喚醒所有正在等待這個(gè)對(duì)象的monitor的線程料身;

上面已經(jīng)提到汤纸,如果調(diào)用某個(gè)對(duì)象的wait()方法,當(dāng)前線程必須擁有這個(gè)對(duì)象的monitor(即鎖)芹血,因此調(diào)用wait()方法必須在同步塊或者同步方法中進(jìn)行(synchronized塊或者synchronized方法)蹲嚣。

調(diào)用某個(gè)對(duì)象的wait()方法,相當(dāng)于讓當(dāng)前線程交出此對(duì)象的monitor祟牲,然后進(jìn)入等待狀態(tài)隙畜,等待后續(xù)再次獲得此對(duì)象的鎖(Thread類中的sleep方法使當(dāng)前線程暫停執(zhí)行一段時(shí)間,從而讓其他線程有機(jī)會(huì)繼續(xù)執(zhí)行说贝,但它并不釋放對(duì)象鎖)议惰;

notify()方法能夠喚醒一個(gè)正在等待該對(duì)象的monitor的線程,當(dāng)有多個(gè)線程都在等待該對(duì)象的monitor的話乡恕,則只能喚醒其中一個(gè)線程言询,具體喚醒哪個(gè)線程則不得而知。

同樣地傲宜,調(diào)用某個(gè)對(duì)象的notify()方法运杭,當(dāng)前線程也必須擁有這個(gè)對(duì)象的monitor,因此調(diào)用notify()方法必須在同步塊或者同步方法中進(jìn)行(synchronized塊或者synchronized方法)函卒。

nofityAll()方法能夠喚醒所有正在等待該對(duì)象的monitor的線程辆憔,這一點(diǎn)與notify()方法是不同的。

這里要注意一點(diǎn):notify()和notifyAll()方法只是喚醒等待該對(duì)象的monitor的線程报嵌,并不決定哪個(gè)線程能夠獲取到monitor虱咧。

舉個(gè)簡(jiǎn)單的例子:假如有三個(gè)線程Thread1、Thread2和Thread3都在等待對(duì)象objectA的monitor锚国,此時(shí)Thread4擁有對(duì)象objectA的monitor腕巡,當(dāng)在Thread4中調(diào)用objectA.notify()方法之后,Thread1血筑、Thread2和Thread3只有一個(gè)能被喚醒绘沉。注意煎楣,被喚醒不等于立刻就獲取了objectA的monitor。假若在Thread4中調(diào)用objectA.notifyAll()方法车伞,則Thread1择懂、Thread2和Thread3三個(gè)線程都會(huì)被喚醒,至于哪個(gè)線程接下來能夠獲取到objectA的monitor就具體依賴于操作系統(tǒng)的調(diào)度了帖世。

上面尤其要注意一點(diǎn)休蟹,一個(gè)線程被喚醒不代表立即獲取了對(duì)象的monitor,只有等調(diào)用完notify()或者notifyAll()并退出synchronized塊日矫,釋放對(duì)象鎖后赂弓,其余線程才可獲得鎖執(zhí)行。

為什么wait()和notify()屬于Object類

wait()和notify()是Java給我們提供線程之間通信的API哪轿,既然是線程的東西盈魁,那什么是在Object類上定義,而不是在Thread類上定義呢窃诉?

其實(shí)這個(gè)問題很簡(jiǎn)單杨耙,由于每個(gè)對(duì)象都擁有monitor(即鎖),所以讓當(dāng)前線程等待某個(gè)對(duì)象的鎖飘痛,當(dāng)然應(yīng)該通過這個(gè)對(duì)象來操作了珊膜。而不是用當(dāng)前線程來操作,因?yàn)楫?dāng)前線程可能會(huì)等待多個(gè)線程的鎖宣脉,如果通過線程來操作车柠,就非常復(fù)雜了。

Java 線程wait()之后一定要notify()才能喚醒嗎塑猖?

不一定竹祷!

線程正常結(jié)束后,會(huì)使以這個(gè)線程對(duì)象運(yùn)行的wait()等待羊苟,退出等待狀態(tài)塑陵!而如果在運(yùn)行wait()之前,線程已經(jīng)結(jié)束了蜡励,則這個(gè)wait就沒有程序喚醒了

實(shí)際上令花,Thread源碼里面的join()方法也是使用這種機(jī)制:

As a thread terminates the {@code this.notifyAll} method is invoked.

源碼里的join()方法,實(shí)際上就是運(yùn)行的 wait(). 需要運(yùn)行join的線程運(yùn)行join方法巍虫,實(shí)際上是在此線程上調(diào)用了需要加入的線程對(duì)象的wait()方法彭则,加入的線程運(yùn)行完后,會(huì)自動(dòng)調(diào)用 notifyAll 方法占遥,被 join 的線程自然能夠繼續(xù)執(zhí)行了。

/**
 * Waits at most {@code millis} milliseconds for this thread to
 * die. A timeout of {@code 0} means to wait forever.
 *
 * <p> This implementation uses a loop of {@code this.wait} calls
 * conditioned on {@code this.isAlive}. As a thread terminates the
 * {@code this.notifyAll} method is invoked. It is recommended that
 * applications not use {@code wait}, {@code notify}, or
 * {@code notifyAll} on {@code Thread} instances.
 *
 * @param  millis
 *         the time to wait in milliseconds
 *
 * @throws  IllegalArgumentException
 *          if the value of {@code millis} is negative
 *
 * @throws  InterruptedException
 *          if any thread has interrupted the current thread. The
 *          <i>interrupted status</i> of the current thread is
 *          cleared when this exception is thrown.
 */
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;
        }
    }
}

線程對(duì)象的wait()方法運(yùn)行后输瓜,可以不用其notify()方法退出瓦胎,會(huì)在線程結(jié)束后芬萍,自動(dòng)退出。

Condition

Condition是在java 1.5中才出現(xiàn)的搔啊,它用來替代傳統(tǒng)的Object的wait()柬祠、notify()實(shí)現(xiàn)線程間的協(xié)作,相比使用Object的wait()负芋、notify()漫蛔,使用Condition1的await()、signal()這種方式實(shí)現(xiàn)線程間協(xié)作更加安全和高效旧蛾。因此通常來說比較推薦使用Condition莽龟,在阻塞隊(duì)列那一篇博文中就講述到了,阻塞隊(duì)列實(shí)際上是使用了Condition來模擬線程間協(xié)作锨天。

  • Condition是個(gè)接口毯盈,基本的方法就是await()和signal()方法;
  • Condition依賴于Lock接口病袄,生成一個(gè)Condition的基本代碼是lock.newCondition()
  • 調(diào)用Condition的await()和signal()方法搂赋,都必須在lock保護(hù)之內(nèi),就是說必須在lock.lock()和lock.unlock之間才可以使用

Conditon中的await()對(duì)應(yīng)Object的wait()益缠;

Condition中的signal()對(duì)應(yīng)Object的notify()脑奠;

Condition中的signalAll()對(duì)應(yīng)Object的notifyAll()。

生產(chǎn)者-消費(fèi)者模型的實(shí)現(xiàn)

1.使用Object的wait()和notify()實(shí)現(xiàn)

public class Test {
    private int queueSize = 10;
    private PriorityQueue<Integer> queue = new PriorityQueue<Integer>(queueSize);
      
    public static void main(String[] args)  {
        Test test = new Test();
        Producer producer = test.new Producer();
        Consumer consumer = test.new Consumer();
          
        producer.start();
        consumer.start();
    }
      
    class Consumer extends Thread{
          
        @Override
        public void run() {
            consume();
        }
          
        private void consume() {
            while(true){
                synchronized (queue) {
                    while(queue.size() == 0){
                        try {
                            System.out.println("隊(duì)列空幅慌,等待數(shù)據(jù)");
                            queue.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            queue.notify();
                        }
                    }
                    queue.poll();          //每次移走隊(duì)首元素
                    queue.notify();
                    System.out.println("從隊(duì)列取走一個(gè)元素宋欺,隊(duì)列剩余"+queue.size()+"個(gè)元素");
                }
            }
        }
    }
      
    class Producer extends Thread{
          
        @Override
        public void run() {
            produce();
        }
          
        private void produce() {
            while(true){
                synchronized (queue) {
                    while(queue.size() == queueSize){
                        try {
                            System.out.println("隊(duì)列滿,等待有空余空間");
                            queue.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            queue.notify();
                        }
                    }
                    queue.offer(1);        //每次插入一個(gè)元素
                    queue.notify();
                    System.out.println("向隊(duì)列取中插入一個(gè)元素欠痴,隊(duì)列剩余空間:"+(queueSize-queue.size()));
                }
            }
        }
    }
}

2.使用Condition實(shí)現(xiàn)

public class Test {
    private int queueSize = 10;
    private PriorityQueue<Integer> queue = new PriorityQueue<Integer>(queueSize);
    private Lock lock = new ReentrantLock();
    private Condition notFull = lock.newCondition();
    private Condition notEmpty = lock.newCondition();
     
    public static void main(String[] args)  {
        Test test = new Test();
        Producer producer = test.new Producer();
        Consumer consumer = test.new Consumer();
          
        producer.start();
        consumer.start();
    }
      
    class Consumer extends Thread{
          
        @Override
        public void run() {
            consume();
        }
          
        private void consume() {
            while(true){
                lock.lock();
                try {
                    while(queue.size() == 0){
                        try {
                            System.out.println("隊(duì)列空迄靠,等待數(shù)據(jù)");
                            notEmpty.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    queue.poll();                //每次移走隊(duì)首元素
                    notFull.signal();
                    System.out.println("從隊(duì)列取走一個(gè)元素,隊(duì)列剩余"+queue.size()+"個(gè)元素");
                } finally{
                    lock.unlock();
                }
            }
        }
    }
      
    class Producer extends Thread{
          
        @Override
        public void run() {
            produce();
        }
          
        private void produce() {
            while(true){
                lock.lock();
                try {
                    while(queue.size() == queueSize){
                        try {
                            System.out.println("隊(duì)列滿喇辽,等待有空余空間");
                            notFull.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    queue.offer(1);        //每次插入一個(gè)元素
                    notEmpty.signal();
                    System.out.println("向隊(duì)列取中插入一個(gè)元素掌挚,隊(duì)列剩余空間:"+(queueSize-queue.size()));
                } finally{
                    lock.unlock();
                }
            }
        }
    }
}

參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市菩咨,隨后出現(xiàn)的幾起案子吠式,更是在濱河造成了極大的恐慌,老刑警劉巖抽米,帶你破解...
    沈念sama閱讀 211,123評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件特占,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡云茸,警方通過查閱死者的電腦和手機(jī)是目,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來标捺,“玉大人懊纳,你說我怎么就攤上這事揉抵。” “怎么了嗤疯?”我有些...
    開封第一講書人閱讀 156,723評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵冤今,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我茂缚,道長(zhǎng)戏罢,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,357評(píng)論 1 283
  • 正文 為了忘掉前任脚囊,我火速辦了婚禮龟糕,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘凑术。我一直安慰自己翩蘸,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評(píng)論 5 384
  • 文/花漫 我一把揭開白布淮逊。 她就那樣靜靜地躺著催首,像睡著了一般。 火紅的嫁衣襯著肌膚如雪泄鹏。 梳的紋絲不亂的頭發(fā)上郎任,一...
    開封第一講書人閱讀 49,760評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音备籽,去河邊找鬼舶治。 笑死,一個(gè)胖子當(dāng)著我的面吹牛车猬,可吹牛的內(nèi)容都是我干的霉猛。 我是一名探鬼主播,決...
    沈念sama閱讀 38,904評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼珠闰,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼惜浅!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起伏嗜,我...
    開封第一講書人閱讀 37,672評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤坛悉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后承绸,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體裸影,經(jīng)...
    沈念sama閱讀 44,118評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評(píng)論 2 325
  • 正文 我和宋清朗相戀三年军熏,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了轩猩。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,599評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖界轩,靈堂內(nèi)的尸體忽然破棺而出画饥,到底是詐尸還是另有隱情衔瓮,我是刑警寧澤浊猾,帶...
    沈念sama閱讀 34,264評(píng)論 4 328
  • 正文 年R本政府宣布,位于F島的核電站热鞍,受9級(jí)特大地震影響葫慎,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜薇宠,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評(píng)論 3 312
  • 文/蒙蒙 一偷办、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧澄港,春花似錦椒涯、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至狱意,卻和暖如春湖苞,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背详囤。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評(píng)論 1 264
  • 我被黑心中介騙來泰國(guó)打工财骨, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人藏姐。 一個(gè)月前我還...
    沈念sama閱讀 46,286評(píng)論 2 360
  • 正文 我出身青樓隆箩,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親羔杨。 傳聞我的和親對(duì)象是個(gè)殘疾皇子捌臊,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評(píng)論 2 348

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