java并發(fā)編程之三:wait/notify/sleep/yield/join

1.線程的狀態(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)換圖

1.wait/notify/notifyAll方法的使用:

JDK中一共提供了這三個(gè)版本的方法,

(1)wait()方法的作用是將當(dāng)前運(yùn)行的線程掛起(即讓其進(jìn)入阻塞狀態(tài)),直到notify或notifyAll方法來喚醒線程.

(2)wait(long timeout),該方法與wait()方法類似,唯一的區(qū)別就是超過指定時(shí)間時(shí),如果還沒有notify或notifAll方法的喚醒,也會(huì)自動(dòng)喚醒。

(3)至于wait(long timeout,long nanos),本意在于更精確的控制調(diào)度時(shí)間,不過從目前版本來看,該方法貌似沒有完整的實(shí)現(xiàn)該功能,其源碼如下:

//Object類:
    public final void wait() throws InterruptedException {
        wait(0);
    }
// Android-changed: Implement wait(long) non-natively.
    // public final native void wait(long timeout) throws InterruptedException;
    public final void wait(long timeout) throws InterruptedException {
        wait(timeout, 0);
    }
   @FastNative
    public final native void wait(long timeout, int nanos) throws InterruptedException;

通過源碼可以看到最終調(diào)用的是底層native方法wait(long timeout, int nanos)。
其實(shí)wait方法底層也是通過對(duì)象鎖監(jiān)視器monitor實(shí)現(xiàn)的,可以通過代碼驗(yàn)證下:

//方法沒有使用synchronized修飾,調(diào)用method方法時(shí)會(huì)拋出IllegalMonitorStateException異常
public void method() throws InterruptedException {
        wait();
    }

//異常
Thrown to indicate that a thread has attempted to wait on an object's monitor or to notify other threads waiting on an object's monitor without owning the specified monitor.
這句話的意思大概就是:線程試圖等待對(duì)象的監(jiān)視器或者試圖通知其他正在等待對(duì)象監(jiān)視器的線程,但本身沒有對(duì)應(yīng)的監(jiān)視器的所有權(quán)。wait方法是一個(gè)本地方法,其底層是通過一個(gè)叫做監(jiān)視器鎖的對(duì)象來完成的。所以上面之所以會(huì)拋出異常,是因?yàn)樵谡{(diào)用wait方式時(shí)沒有獲取到monitor對(duì)象的所有權(quán),那如何獲取monitor對(duì)象所有權(quán)?Java中只能通過Synchronized關(guān)鍵字來完成,修改上述代碼,增加Synchronized關(guān)鍵字:

public synchronized void method() throws InterruptedException {
        wait();
    }

有了對(duì)wait方法原理的理解,notify方法和notifyAll方法就很容易理解了。既然wait方式是通過對(duì)象的monitor對(duì)象來實(shí)現(xiàn)的,所以只要在同一對(duì)象上去調(diào)用notify/notifyAll方法,就可以喚醒對(duì)應(yīng)對(duì)象monitor上等待的線程了。notify和notifyAll的區(qū)別在于前者只能喚醒monitor上的一個(gè)線程,對(duì)其他線程沒有影響,而notifyAll則喚醒所有的線程.
注意:調(diào)用wait方法后,線程是會(huì)釋放對(duì)monitor對(duì)象的所有權(quán)的,即會(huì)釋放對(duì)象鎖,這點(diǎn)和sleep不同。

2. sleep/yield/join方法解析

這三個(gè)方法都位于Thread類中是靜態(tài)方法,而上面三個(gè)方法都位于Object類中。

2.1 sleep(long timeout)

sleep方法的作用是讓當(dāng)前線程暫停指定的時(shí)間(毫秒),sleep方法是最簡(jiǎn)單的方法,在上述的例子中也用到過,比較容易理解。唯一需要注意的是其與wait方法的區(qū)別。最簡(jiǎn)單的區(qū)別是,wait方法依賴于同步,而sleep方法可以直接調(diào)用。而更深層次的區(qū)別在于sleep方法只是暫時(shí)讓出CPU的執(zhí)行權(quán),并不釋放鎖。而wait方法則需要釋放鎖。sleep暫停期間一直持有monitor對(duì)象鎖,其他線程是不能進(jìn)入的。而wait方法則不同,當(dāng)調(diào)用wait方法后,當(dāng)前線程會(huì)釋放持有的monitor對(duì)象鎖,因此,其他線程還可以進(jìn)入到同步方法,線程被喚醒后,需要競(jìng)爭(zhēng)鎖,獲取到鎖之后再繼續(xù)執(zhí)行

2.2 yield

yield方法的作用是暫停當(dāng)前線程,以便其他線程有機(jī)會(huì)執(zhí)行,不過不能指定暫停的時(shí)間,并且也不能保證當(dāng)前線程馬上停止。yield方法只是將Running運(yùn)行狀態(tài)轉(zhuǎn)變?yōu)镽unnable就緒狀態(tài)。

2.3 join()/join(long timeout)

join方法的作用是父線程等待子線程執(zhí)行完成后再執(zhí)行,換句話說就是將異步執(zhí)行的線程合并為同步的線程。其實(shí)現(xiàn)與wait方法類似,join()方法實(shí)際上執(zhí)行的join(0),而join(long millis)也與wait(long millis)的實(shí)現(xiàn)方式一致,join內(nèi)部是通過wait來實(shí)現(xiàn)的,可以在源碼中查看:

/* @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 void join() throws InterruptedException {
        join(0);
    }

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

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

        if (millis == 0) {
            while (isAlive()) {
                lock.wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                lock.wait(delay);//調(diào)用wait方法,但是哪里調(diào)用的lock.notify/notifyAll()喚醒的呢
                now = System.currentTimeMillis() - base;
            }
        }
        }
    }

大家重點(diǎn)關(guān)注一下join(long millis)方法的實(shí)現(xiàn),可以看出join方法就是通過wait方法來將線程的阻塞,如果join的線程還在執(zhí)行,則將當(dāng)前線程阻塞起來,直到j(luò)oin的線程執(zhí)行完成,當(dāng)前線程才能執(zhí)行。不過有一點(diǎn)需要注意,這里的join只調(diào)用了wait方法,卻沒有對(duì)應(yīng)的notify方法,原因是Thread的start方法中做了相應(yīng)的處理,所以當(dāng)join的線程執(zhí)行完成以后,會(huì)自動(dòng)喚醒主線程繼續(xù)往下執(zhí)行。

總結(jié)

wait/notify/notifyAll方法的作用是實(shí)現(xiàn)線程間的協(xié)作,那為什么這三個(gè)方法不是位于Thread類中,而是位于Object類中?位于Object中,也就相當(dāng)于所有類都包含這三個(gè)方法(因?yàn)镴ava中所有的類都繼承自O(shè)bject類)。要回答這個(gè)問題,還是得回過來看wait方法的實(shí)現(xiàn)原理,大家需要明白的是,wait等待的到底是什么東西?如果對(duì)上面內(nèi)容理解的比較好的話,我相信大家應(yīng)該很容易知道wait等待其實(shí)是對(duì)象monitor監(jiān)視器對(duì)象,因?yàn)镴ava中的每一個(gè)對(duì)象都有一個(gè)內(nèi)置的monitor對(duì)象,自然所有的類都理應(yīng)有wait/notify方法。

區(qū)別
notify:只會(huì)喚醒等待該鎖的其中一個(gè)線程。
notifyAll:?jiǎn)拘训却撴i的所有線程。
既然notify會(huì)喚醒一個(gè)線程,并獲取鎖,notifyAll會(huì)喚醒所有線程并根據(jù)算法選取其中一個(gè)線程獲取鎖,那最終結(jié)果不都是只有一個(gè)線程獲取鎖嗎?那JDK為什么還需要做出來這兩個(gè)方法呢?這兩種同步方法本質(zhì)上會(huì)有什么區(qū)別?

這還要從對(duì)象內(nèi)部鎖的調(diào)度說起。

對(duì)象內(nèi)部鎖
其實(shí),每個(gè)對(duì)象都擁有兩個(gè)池,分別為鎖池(EntrySet)和(WaitSet)等待池。

鎖池:假如已經(jīng)有線程A獲取到了鎖,這時(shí)候又有線程B需要獲取這把鎖(比如需要調(diào)用synchronized修飾的方法或者需要執(zhí)行synchronized修飾的代碼塊),由于該鎖已經(jīng)被占用,所以線程B只能等待這把鎖,這時(shí)候線程B將會(huì)進(jìn)入這把鎖的鎖池。
等待池:假設(shè)線程A獲取到鎖之后,由于一些條件的不滿足(例如生產(chǎn)者消費(fèi)者模式中生產(chǎn)者獲取到鎖,然后判斷隊(duì)列為滿),此時(shí)需要調(diào)用對(duì)象鎖的wait方法,那么線程A將放棄這把鎖,并進(jìn)入這把鎖的等待池。
如果有其他線程調(diào)用了鎖的notify方法,則會(huì)根據(jù)一定的算法從等待池中選取一個(gè)線程,將此線程放入鎖池。
如果有其他線程調(diào)用了鎖的notifyAll方法,則會(huì)將等待池中所有線程全部放入鎖池,并爭(zhēng)搶鎖。

鎖池與等待池的區(qū)別:等待池中的線程不能獲取鎖,而是需要被喚醒進(jìn)入鎖池,才有獲取到鎖的機(jī)會(huì)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容