溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

線程間的協(xié)作有哪些

發(fā)布時(shí)間:2021-10-23 17:02:34 來源:億速云 閱讀:86 作者:iii 欄目:編程語言

本篇內(nèi)容介紹了“線程間的協(xié)作有哪些”的有關(guān)知識,在實(shí)際案例的操作過程中,不少人都會(huì)遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!

一、線程的狀態(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)系如下圖所示:

線程間的協(xié)作有哪些

有了對這五種狀態(tài)的基本了解,現(xiàn)在我們來看看Java中是如何實(shí)現(xiàn)這幾種狀態(tài)的轉(zhuǎn)換的。 

大家好,我是AIO生活,關(guān)注我,后續(xù)連載更多技術(shù)重難點(diǎn),文章有不足之處,歡迎大家留言指正,謝謝大家啦!

順便提一下,幫大家整理了些JAVA電子書,掃碼回復(fù)“1024”獲取電子書合集,也可簽到領(lǐng)現(xiàn)金紅包。

暖冬回血~~~~

線程間的協(xié)作有哪些

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

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

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

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

     

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 >= 500000 || (nanos != 0 && timeout == 0)) {
            timeout++;
        }
 
        wait(timeout);
    }

從源碼來看,JDK8中對納秒的處理,只做了四舍五入,所以還是按照毫秒來處理的,可能在未來的某個(gè)時(shí)間點(diǎn)會(huì)用到納秒級別的精度。雖然JDK提供了這三個(gè)版本,其實(shí)最后都是調(diào)用wait(long timeout)方法來實(shí)現(xiàn)的,wait()方法與wait(0)等效,而wait(long timeout,int nanos)從上面的源碼可以看到也是通過wait(long timeout)來完成的。

下面我們通過一個(gè)簡單的例子來演示wait()方法的使用:

package com.paddx.test.concurrent;
 
public class WaitTest {
 
    public void testWait(){
        System.out.println("Start-----");
        try {
            wait(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("End-------");
    }
 
    public static void main(String[] args) {
        final WaitTest test = new WaitTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test.testWait();
            }
        }).start();
    }
}

這段代碼的意圖很簡單,就是程序執(zhí)行以后,讓其暫停一秒,然后再執(zhí)行。運(yùn)行上述代碼,查看結(jié)果:

Start-----
Exception in thread "Thread-0">這段程序并沒有按我們的預(yù)期輸出相應(yīng)結(jié)果,而是拋出了一個(gè)異常。大家可能會(huì)覺得奇怪為什么會(huì)拋出異常?而拋出的IllegalMonitorStateException異常又是什么?我們可以看一下JDK中對IllegalMonitorStateException的描述: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.這句話的意思大概就是:線程試圖等待對象的監(jiān)視器或者試圖通知其他正在等待對象監(jiān)視器的線程,但本身沒有對應(yīng)的監(jiān)視器的所有權(quán)。其實(shí)這個(gè)問題在《【68期】面試官:對并發(fā)熟悉嗎?說說Synchronized及實(shí)現(xiàn)原理》一文中有提到過,wait方法是一個(gè)本地方法,其底層是通過一個(gè)叫做監(jiān)視器鎖的對象來完成的。所以上面之所以會(huì)拋出異常,是因?yàn)樵谡{(diào)用wait方式時(shí)沒有獲取到monitor對象的所有權(quán),那如何獲取monitor對象所有權(quán)?Java中只能通過Synchronized關(guān)鍵字來完成,修改上述代碼,增加Synchronized關(guān)鍵字:package com.paddx.test.concurrent;
 
public class WaitTest {
 
    public synchronized void testWait(){//增加Synchronized關(guān)鍵字
        System.out.println("Start-----");
        try {
            wait(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("End-------");
    }
 
    public static void main(String[] args) {
        final WaitTest test = new WaitTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test.testWait();
            }
        }).start();
    }
} 現(xiàn)在再運(yùn)行上述代碼,就能看到預(yù)期的效果了:Start-----
End------- 所以,通過這個(gè)例子,大家應(yīng)該很清楚,wait方法的使用必須在同步的范圍內(nèi),否則就會(huì)拋出IllegalMonitorStateException異常,wait方法的作用就是阻塞當(dāng)前線程等待notify/notifyAll方法的喚醒,或等待超時(shí)后自動(dòng)喚醒。 2、notify/notifyAll方法有了對wait方法原理的理解,notify方法和notifyAll方法就很容易理解了。既然wait方式是通過對象的monitor對象來實(shí)現(xiàn)的,所以只要在同一對象上去調(diào)用notify/notifyAll方法,就可以喚醒對應(yīng)對象monitor上等待的線程了。notify和notifyAll的區(qū)別在于前者只能喚醒monitor上的一個(gè)線程,對其他線程沒有影響,而notifyAll則喚醒所有的線程,看下面的例子很容易理解這兩者的差別:package com.paddx.test.concurrent;
 
public class NotifyTest {
    public synchronized void testWait(){
        System.out.println(Thread.currentThread().getName() +"> 輸出結(jié)果如下:Thread-0 Start-----
Thread-1 Start-----
Thread-2 Start-----
Thread-3 Start-----
Thread-4 Start-----
Thread-0 End-------
-----------分割線-------------
Thread-4 End-------
Thread-3 End-------
Thread-2 End-------
Thread-1 End------- 從結(jié)果可以看出:調(diào)用notify方法時(shí)只有線程Thread-0被喚醒,但是調(diào)用notifyAll時(shí),所有的線程都被喚醒了。最后,有兩點(diǎn)需要注意:1.調(diào)用wait方法后,線程是會(huì)釋放對monitor對象的所有權(quán)的。2.一個(gè)通過wait方法阻塞的線程,必須同時(shí)滿足以下兩個(gè)條件才能被真正執(zhí)行:線程需要被喚醒(超時(shí)喚醒或調(diào)用notify/notifyll)。線程喚醒后需要競爭到鎖(monitor)。 三、sleep/yield/join方法解析上面我們已經(jīng)清楚了wait和notify方法的使用和原理,現(xiàn)在我們再來看另外一組線程間協(xié)作的方法。這組方法跟上面方法的最明顯區(qū)別是:這幾個(gè)方法都位于Thread類中,而上面三個(gè)方法都位于Object類中。至于為什么,大家可以先思考一下。現(xiàn)在我們逐個(gè)分析sleep/yield/join方法: 1、sleepsleep方法的作用是讓當(dāng)前線程暫停指定的時(shí)間(毫秒),sleep方法是最簡單的方法,在上述的例子中也用到過,比較容易理解。唯一需要注意的是其與wait方法的區(qū)別。最簡單的區(qū)別是,wait方法依賴于同步,而sleep方法可以直接調(diào)用。而更深層次的區(qū)別在于sleep方法只是暫時(shí)讓出CPU的執(zhí)行權(quán),并不釋放鎖。而wait方法則需要釋放鎖。package com.paddx.test.concurrent;
 
public class SleepTest {
    public synchronized void sleepMethod(){
        System.out.println("Sleep start-----");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Sleep end-----");
    }
 
    public synchronized void waitMethod(){
        System.out.println("Wait start-----");
        synchronized (this){
            try {
                wait(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("Wait end-----");
    }
 
    public static void main(String[] args) {
        final SleepTest test1 = new SleepTest();
 
        for(int i = 0;i<3;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    test1.sleepMethod();
                }
            }).start();
        }
 
 
        try {
            Thread.sleep(10000);//暫停十秒,等上面程序執(zhí)行完成
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("-----分割線-----");
 
        final SleepTest test2 = new SleepTest();
 
        for(int i = 0;i<3;i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    test2.waitMethod();
                }
            }).start();
        }
 
    }
} 執(zhí)行結(jié)果:Sleep start-----
Sleep end-----
Sleep start-----
Sleep end-----
Sleep start-----
Sleep end-----
-----分割線-----
Wait start-----
Wait start-----
Wait start-----
Wait end-----
Wait end-----
Wait end-----  這個(gè)結(jié)果的區(qū)別很明顯,通過sleep方法實(shí)現(xiàn)的暫停,程序是順序進(jìn)入同步塊的,只有當(dāng)上一個(gè)線程執(zhí)行完成的時(shí)候,下一個(gè)線程才能進(jìn)入同步方法,sleep暫停期間一直持有monitor對象鎖,其他線程是不能進(jìn)入的。而wait方法則不同,當(dāng)調(diào)用wait方法后,當(dāng)前線程會(huì)釋放持有的monitor對象鎖,因此,其他線程還可以進(jìn)入到同步方法,線程被喚醒后,需要競爭鎖,獲取到鎖之后再繼續(xù)執(zhí)行。 2、yield方法yield方法的作用是暫停當(dāng)前線程,以便其他線程有機(jī)會(huì)執(zhí)行,不過不能指定暫停的時(shí)間,并且也不能保證當(dāng)前線程馬上停止。yield方法只是將Running狀態(tài)轉(zhuǎn)變?yōu)镽unnable狀態(tài)。我們還是通過一個(gè)例子來演示其使用:package com.paddx.test.concurrent;
 
public class YieldTest implements Runnable {
    @Override
    public void run() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for(int i=0;i<5;i++){
            System.out.println(Thread.currentThread().getName() + ": " + i);
            Thread.yield();
        }
    }
 
    public static void main(String[] args) {
        YieldTest runn = new YieldTest();
        Thread t1 = new Thread(runn,"FirstThread");
        Thread t2 = new Thread(runn,"SecondThread");
 
        t1.start();
        t2.start();
 
    }
} 運(yùn)行結(jié)果如下:FirstThread: 0
SecondThread: 0
FirstThread: 1
SecondThread: 1
FirstThread: 2
SecondThread: 2
FirstThread: 3
SecondThread: 3
FirstThread: 4
SecondThread: 4 這個(gè)例子就是通過yield方法來實(shí)現(xiàn)兩個(gè)線程的交替執(zhí)行。不過請注意:這種交替并不一定能得到保證,源碼中也對這個(gè)問題進(jìn)行說明:/**
     * A hint to the scheduler that the current thread is willing to yield
     * its current use of a processor. The scheduler is free to ignore this
     * hint.
     *
     * <p> Yield is a heuristic attempt to improve relative progression
     * between threads that would otherwise over-utilise a CPU. Its use
     * should be combined with detailed profiling and benchmarking to
     * ensure that it actually has the desired effect.
     *
     * <p> It is rarely appropriate to use this method. It may be useful
     * for debugging or testing purposes, where it may help to reproduce
     * bugs due to race conditions. It may also be useful when designing
     * concurrency control constructs such as the ones in the
     * {@link java.util.concurrent.locks} package.
*/  這段話主要說明了三個(gè)問題:調(diào)度器可能會(huì)忽略該方法。使用的時(shí)候要仔細(xì)分析和測試,確保能達(dá)到預(yù)期的效果。很少有場景要用到該方法,主要使用的地方是調(diào)試和測試。   3、join方法join方法的作用是父線程等待子線程執(zhí)行完成后再執(zhí)行,換句話說就是將異步執(zhí)行的線程合并為同步的線程。JDK中提供三個(gè)版本的join方法,其實(shí)現(xiàn)與wait方法類似,join()方法實(shí)際上執(zhí)行的join(0),而join(long millis, int nanos)也與wait(long millis, int nanos)的實(shí)現(xiàn)方式一致,暫時(shí)對納秒的支持也是不完整的。我們可以看下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;
            }
        }
    }
 
public final synchronized void join(long millis, int nanos)
    throws InterruptedException {
 
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
 
        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }
 
        if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
            millis++;
        }
 
        join(millis);
    }  大家重點(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方法,卻沒有對應(yīng)的notify方法,原因是Thread的start方法中做了相應(yīng)的處理,所以當(dāng)join的線程執(zhí)行完成以后,會(huì)自動(dòng)喚醒主線程繼續(xù)往下執(zhí)行。下面我們通過一個(gè)例子來演示join方法的作用:(1)不使用join方法:package com.paddx.test.concurrent;
 
public class JoinTest implements Runnable{
    @Override
    public void run() {
 
        try {
            System.out.println(Thread.currentThread().getName() + ">執(zhí)行結(jié)果如下:Thread-0 start-----
Thread-1 start-----
Thread-2 start-----
Thread-3 start-----
Finished~~~
Thread-4 start-----
Thread-2 end------
Thread-4 end------
Thread-1 end------
Thread-0 end------
Thread-3 end------ (2)使用join方法:package com.paddx.test.concurrent;
 
public class JoinTest implements Runnable{
    @Override
    public void run() {
 
        try {
            System.out.println(Thread.currentThread().getName() + " start-----");
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + " end------");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
 
    public static void main(String[] args) {
        for (int i=0;i<5;i++) {
            Thread test = new Thread(new JoinTest());
            test.start();
            try {
                test.join(); //調(diào)用join方法
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
 
        System.out.println("Finished~~~");
    }
} 執(zhí)行結(jié)果如下:Thread-0 start-----
Thread-0 end------
Thread-1 start-----
Thread-1 end------
Thread-2 start-----
Thread-2 end------
Thread-3 start-----
Thread-3 end------
Thread-4 start-----
Thread-4 end------
Finished~~~  對比兩段代碼的執(zhí)行結(jié)果很容易發(fā)現(xiàn),在沒有使用join方法之間,線程是并發(fā)執(zhí)行的,而使用join方法后,所有線程是順序執(zhí)行的。 四、總結(jié)本文主要詳細(xì)講解了wait/notify/notifyAll和sleep/yield/join方法。最后回答一下上面提出的問題: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等待的到底是什么東西?如果對上面內(nèi)容理解的比較好的話,我相信大家應(yīng)該很容易知道wait等待其實(shí)是對象monitor,由于Java中的每一個(gè)對象都有一個(gè)內(nèi)置的monitor對象,自然所有的類都理應(yīng)有wait/notify方法。暖冬回血~~~~大家好,我是AIO生活,關(guān)注我,后續(xù)連載更多技術(shù)重難點(diǎn),文章有不足之處,歡迎大家留言指正,謝謝大家啦!順便提一下,幫大家整理了些JAVA電子書,掃碼回復(fù)“1024”獲取電子書合集,也可簽到領(lǐng)現(xiàn)金紅包。

“線程間的協(xié)作有哪些”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實(shí)用文章!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI