線(xiàn)程間的協(xié)作方法(wait、notify、sleep、yield、join、interrupt、notifyAll)

線(xiàn)程的狀態(tài)

萬(wàn)事萬(wàn)物都有其自己的生命周期和狀態(tài),一個(gè)線(xiàn)程從創(chuàng)建到結(jié)束被銷(xiāo)毀也有其自己的六種狀態(tài),而wait、notify、sleep等等這些方法就是協(xié)助切換線(xiàn)程間的狀態(tài)

Oracle官方文檔提供的六種線(xiàn)程狀態(tài)

狀態(tài)名稱(chēng) 說(shuō)明
NEW 初始狀態(tài),線(xiàn)程被創(chuàng)建,但是還沒(méi)有調(diào)用start()方法,線(xiàn)程還未被啟動(dòng)
RUNNABLE 運(yùn)行狀態(tài),一個(gè)線(xiàn)程開(kāi)始在java虛擬機(jī)中被執(zhí)行
BLOCKED 阻塞狀態(tài),線(xiàn)程被鎖住等待獲得對(duì)象的monitor lock,換言之就是被鎖(Synchronize)阻塞了
WAITING 等待狀態(tài),無(wú)限期等待另一個(gè)線(xiàn)程執(zhí)行特定操作的線(xiàn)程處于此狀態(tài)。
TIMED_WAITING 超時(shí)等待狀態(tài),在指定的等待時(shí)間內(nèi)等待另一個(gè)線(xiàn)程執(zhí)行操作的線(xiàn)程處于此狀態(tài)。
TERMINATED 終止?fàn)顟B(tài),線(xiàn)程執(zhí)行完畢已經(jīng)退出

用一張圖可以清晰的表示上述狀態(tài)在線(xiàn)程中的運(yùn)行狀態(tài)切換


《JAVA并發(fā)編程的藝術(shù)》一書(shū)中的線(xiàn)程狀態(tài)轉(zhuǎn)換圖

線(xiàn)程的狀態(tài)切換的操作

建立線(xiàn)程后我們會(huì)根據(jù)需求對(duì)線(xiàn)程進(jìn)行一些操作,這些操作會(huì)改變線(xiàn)程的基本狀態(tài),同事也成為了線(xiàn)程間的一種通信方式,下面就主要聊聊這些方法。

  • wait()、notify()和notifyAll()

    wait方法主要是將當(dāng)前運(yùn)行的線(xiàn)程掛起,讓其進(jìn)入阻塞狀態(tài),然后釋放它持有的同步鎖(也就是前面文章提到的monitor),通知其他線(xiàn)程來(lái)獲取執(zhí)行,直到notifynotifyAll方法來(lái)喚醒。

    wait也是一個(gè)多參數(shù)方法,可以通過(guò)wait(long timeout)來(lái)設(shè)定線(xiàn)程在指定時(shí)間內(nèi)如果沒(méi)有notifynotifyAll方法的喚醒,也會(huì)自動(dòng)喚醒,wait方法調(diào)用的也是這個(gè)方法,不過(guò)傳入的參數(shù)為0L。

    在使用wait方法時(shí),一定要在同步范圍內(nèi),否則就會(huì)拋出IllegalMonitorStateException異常。

public class SynchronizedDemo {
    public static void main(String[] args) {
        final SynchronizedDemo test = new SynchronizedDemo();
        new Thread(new Runnable() {
            @Override
            public void run() {
                test.waitDemo();
            }
        }).start();
    }

     private void waitDemo() {
        System.out.println("Start Thread"+System.currentTimeMillis());
        try {
            wait(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("End Thread"+System.currentTimeMillis());
    }
}
運(yùn)行結(jié)果:
Start Thread1557818387416
Exception in thread "Thread-0" java.lang.IllegalMonitorStateException
    at java.lang.Object.wait(Native Method)
    at com.example.javalib.SynchronizedDemo.waitDemo(SynchronizedDemo.java:24)
    at com.example.javalib.SynchronizedDemo.access$000(SynchronizedDemo.java:10)
    at com.example.javalib.SynchronizedDemo$1.run(SynchronizedDemo.java:16)
    at java.lang.Thread.run(Thread.java:745)

查看API文檔對(duì)于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.

該錯(cuò)誤的大意為:線(xiàn)程試圖等待一個(gè)對(duì)象的監(jiān)視器或者去通知其他在等待對(duì)象監(jiān)視器的線(xiàn)程,但是該線(xiàn)程本身沒(méi)有持有指定的監(jiān)視器.主要是因?yàn)檎{(diào)用wait方法時(shí)沒(méi)有獲取到對(duì)象的monitor,獲得的途徑可以通過(guò)Synchronized關(guān)鍵字來(lái)完成,在上述代碼的方法中添加Synchronized關(guān)鍵字

private synchronized void waitDemo() {
        System.out.println("Start Thread"+System.currentTimeMillis());
        try {
            wait(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("End Thread"+System.currentTimeMillis());
    }

通過(guò)這個(gè)例子得知,wait方法的使用必須在同步的范圍內(nèi),否則就會(huì)拋出IllegalMonitorStateException異常,wait方法的作用就是阻塞當(dāng)前線(xiàn)程等待notify/notifyAll方法的喚醒,或等待超時(shí)后自動(dòng)喚醒。

wait方法通過(guò)釋放對(duì)象的monitor來(lái)掛起線(xiàn)程,進(jìn)入WaitSet隊(duì)列, 然后后續(xù)等待鎖線(xiàn)程繼續(xù)來(lái)執(zhí)行,直到同一對(duì)象上調(diào)用notifynotifyAll后才可以喚醒等待線(xiàn)程。

notify 和 notifyAll的區(qū)別是notify方法只喚醒一個(gè)等待(對(duì)象的)線(xiàn)程并使該線(xiàn)程開(kāi)始執(zhí)行,如果有多個(gè)線(xiàn)程等待一個(gè)對(duì)象,那么只會(huì)隨機(jī)喚醒其中一個(gè)線(xiàn)程,后者則會(huì)喚醒所有等待(對(duì)象的)線(xiàn)程,哪個(gè)線(xiàn)程第一個(gè)被喚醒也是取決于操作系統(tǒng)。

負(fù)責(zé)調(diào)用方法去喚醒線(xiàn)程的線(xiàn)程也被稱(chēng)為喚醒線(xiàn)程,喚醒線(xiàn)程后不能被立刻執(zhí)行,因?yàn)閱拘丫€(xiàn)程還持有該對(duì)象的同步鎖,必須等待喚醒線(xiàn)程執(zhí)行完畢后釋放了對(duì)象的同步鎖后,等待線(xiàn)程才能獲取到對(duì)象的同步鎖進(jìn)而繼續(xù)執(zhí)行。

從上述中可以看到wait,notify,notifyAll方法的調(diào)用去掛起喚醒線(xiàn)程主要是操作對(duì)象的monitor,而monitor是所有對(duì)象的對(duì)象頭里都擁有的,所以這三個(gè)方法定義在Object類(lèi)中,而不是Thread類(lèi)中

下面一個(gè)用經(jīng)典面試題:雙線(xiàn)程打印奇偶數(shù)來(lái)展示wait和notify的用法(代碼隨便寫(xiě)的,理會(huì)意思就行)

public class Main {
    Object odd = new Object(); // 奇數(shù)條件鎖
    Object even = new Object(); // 偶數(shù)條件鎖
    private int max=200;
    private AtomicInteger status = new AtomicInteger(0); // AtomicInteger保證可見(jiàn)性,也可以用volatile

    public Main() {
    }

    public static void main(String[] args) {
        Main main = new Main();
        Thread printer1 = new Thread(main.new MyPrinter("線(xiàn)程1", 0));
        Thread printer2 = new Thread(main.new MyPrinter("線(xiàn)程2", 1));
        printer1.start();
        printer2.start();
    }
    public class MyPrinter2 implements Runnable {
        private String name;
        private int type; // 打印的類(lèi)型,0:代表打印奇數(shù),1:代表打印偶數(shù)

        public MyPrinter2(String name, int type) {
            this.name = name;
            this.type = type;
        }
        @Override
        public void run() {
            ThreadBean bean = new ThreadBean();
            bean.start(name);
        }
    }
    public class MyPrinter implements Runnable {
        private String name;
        private int type; // 打印的類(lèi)型,0:代表打印奇數(shù),1:代表打印偶數(shù)

        public MyPrinter(String name, int type) {
            this.name = name;
            this.type = type;
        }

        @Override
        public void run() {
            if (type == 0){
                while(status.get()<20){
                    if(status.get()%2==0){
                        synchronized (even){
                            try {
                                even.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }else{
                        synchronized (odd){
                            System.out.println("當(dāng)前是"+name+"輸出"+status.get());
                            status.set(status.get()+1);
                            odd.notify();
                        }
                    }
                }
            }else{
                while(status.get()<20){
                    if(status.get()%2==1){
                        synchronized (odd){
                            try {
                                odd.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }else{
                        synchronized (even){
                            System.out.println("當(dāng)前是"+name+"輸出"+status.get());
                            status.set(status.get()+1);
                            even.notify();
                        }
                    }
                }
            }
        }
    }
}

  • yield

yield是一個(gè)靜態(tài)的原生native方法,他的作用是讓出當(dāng)前線(xiàn)程的CPU分配的時(shí)間片,將其分配給和當(dāng)前線(xiàn)程同優(yōu)先級(jí)的線(xiàn)程,然后當(dāng)前線(xiàn)程狀態(tài)由運(yùn)行中(RUNNING)轉(zhuǎn)換為可運(yùn)行(RUNNABLE)狀態(tài),但這個(gè)并不是等待或者阻塞狀態(tài),也不會(huì)釋放對(duì)象鎖,如果在下一次競(jìng)爭(zhēng)中,又獲得了CPU時(shí)間片當(dāng)前線(xiàn)程依然會(huì)繼續(xù)運(yùn)行。

現(xiàn)在的操作系統(tǒng)中包含多個(gè)進(jìn)程,一個(gè)進(jìn)程又包含多個(gè)線(xiàn)程,那么這些多線(xiàn)程是一起執(zhí)行的嗎?就像電腦上,我們可以一邊看電視一邊瀏覽網(wǎng)頁(yè),其實(shí)并不然,看視兩邊同步進(jìn)行的,但其實(shí)是cpu讓兩個(gè)線(xiàn)程交替執(zhí)行,只不過(guò)交替執(zhí)行的速度很快,肉眼分辨不出來(lái),所以才會(huì)有同步執(zhí)行的錯(cuò)覺(jué)。同理,這里也是一樣,系統(tǒng)會(huì)分出一個(gè)個(gè)時(shí)間片,線(xiàn)程會(huì)被分配到屬于自己執(zhí)行的時(shí)間片,當(dāng)前線(xiàn)程的時(shí)間片用完后會(huì)等待下次分配,線(xiàn)程分配的時(shí)間多少也覺(jué)得了線(xiàn)程使用多少處理器的資源,線(xiàn)程優(yōu)先級(jí)也就是覺(jué)得線(xiàn)程是分配多一些還是少一些處理器的資源

Java中,通過(guò)一個(gè)整型變量Priority來(lái)控制線(xiàn)程的優(yōu)先級(jí),范圍為1~10,通過(guò)調(diào)用setPriority(int Priority)可以設(shè)置,默認(rèn)值為5。

yield一樣,sleep也調(diào)用時(shí)也會(huì)交出當(dāng)前線(xiàn)程的處理器資源,但是不同的是sleep交出的資源所有線(xiàn)程都可以去競(jìng)爭(zhēng),yield交出的時(shí)間片資源只有和當(dāng)前線(xiàn)程同優(yōu)先級(jí)的線(xiàn)程才可以獲取到。

  • join

join方法的作用是父線(xiàn)程(一般是main主線(xiàn)程)等待子線(xiàn)程執(zhí)行完成后再執(zhí)行,換言之就是講異步執(zhí)行的線(xiàn)程合并為同步的主線(xiàn)程,。

wait一樣,join方法也有多個(gè)參數(shù)的方法,也可以設(shè)定超時(shí)時(shí)間,join()方法調(diào)用的也是join(0L)

public class JoinDemo {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("主線(xiàn)程開(kāi)始"+"時(shí)間:"+System.currentTimeMillis());
        JoinDemo main = new JoinDemo();
        Thread printer1 = new Thread(main.new MyPrinter("線(xiàn)程1"));
        Thread printer2 = new Thread(main.new MyPrinter("線(xiàn)程2"));
        Thread printer3 = new Thread(main.new MyPrinter("線(xiàn)程3"));
        printer1.start();
        printer1.join();
        printer2.start();
        printer2.join();
        printer3.start();
        System.out.println("主線(xiàn)程結(jié)束"+"時(shí)間:"+System.currentTimeMillis());
    }

    public class MyPrinter implements Runnable {
        String content;

        public MyPrinter(String content) {
            this.content = content;
        }

        @Override
        public void run() {
            System.out.println("當(dāng)前線(xiàn)程"+content+"時(shí)間:"+System.currentTimeMillis());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
輸出結(jié)果:
主線(xiàn)程開(kāi)始時(shí)間:1557824674063
當(dāng)前線(xiàn)程線(xiàn)程1時(shí)間:1557824674063
當(dāng)前線(xiàn)程線(xiàn)程2時(shí)間:1557824675065
主線(xiàn)程結(jié)束時(shí)間:1557824676065
當(dāng)前線(xiàn)程線(xiàn)程3時(shí)間:1557824676065

從上面例子可以看到線(xiàn)程1和2調(diào)用了join方法后,主線(xiàn)程是等待兩個(gè)線(xiàn)程執(zhí)行完成之后才會(huì)繼續(xù)執(zhí)行

  • interrupt

interrupt的目的是為了中斷線(xiàn)程,原來(lái)Thread.stop, Thread.suspend, Thread.resume 都有這個(gè)功能,但由于都太暴力了而被廢棄了,暴力中斷線(xiàn)程是一種不安全的操作,相對(duì)而言interrupt通過(guò)設(shè)置標(biāo)志位的方式就比較溫柔

interrupt基于一個(gè)線(xiàn)程不應(yīng)該由其他線(xiàn)程來(lái)強(qiáng)制中斷或停止,而是應(yīng)該由線(xiàn)程內(nèi)部來(lái)自行停止的思想來(lái)實(shí)現(xiàn)的,自己的事自己處理,是一種比較溫柔和安全的做法,而且中斷不活動(dòng)的線(xiàn)程不會(huì)產(chǎn)生任何影響。

從API文檔的中的介紹來(lái)看interrupt()的作用是中斷本線(xiàn)程。除非當(dāng)前線(xiàn)程正在中斷自身(始終允許),否則將調(diào)用此線(xiàn)程的checkAccess方法,但這可能導(dǎo)致拋出SecurityException。

如果在調(diào)用Object類(lèi)的wait()、join()、sleep(long)阻塞了這個(gè)線(xiàn)程,那么它的中斷狀態(tài)將被清除并收到InterruptedException。

如果在InterruptibleChannel上的I / O操作中阻塞了該線(xiàn)程,則該通道將被關(guān)閉,線(xiàn)程的中斷狀態(tài)將被設(shè)置,并且線(xiàn)程將收到ClosedByInterruptException。

  • 終止阻塞線(xiàn)程

例如,線(xiàn)程通過(guò)wait()進(jìn)入阻塞狀態(tài),此時(shí)通過(guò)interrupt()中斷該線(xiàn)程;調(diào)用interrupt()會(huì)立即將線(xiàn)程的中斷標(biāo)記設(shè)為“true”,但是由于線(xiàn)程處于阻塞狀態(tài),所以該“中斷標(biāo)記”會(huì)立即被清除為“false”,同時(shí),會(huì)產(chǎn)生一個(gè)InterruptedException的異常。此時(shí)將InterruptedException放在適當(dāng)?shù)奈恢眠M(jìn)行捕獲就能終止阻塞中的線(xiàn)程,如下代碼,將中斷的捕獲放在while(true)之外,就可以退出while循環(huán)

@Override
public void run() {
    try {
        while (true) {
            // 執(zhí)行任務(wù)...
        }
    } catch (InterruptedException ie) {  
        // 由于產(chǎn)生InterruptedException異常,退出while(true)循環(huán),線(xiàn)程終止!
    }
}

但是如果需要將··InterruptedException··在··while(true)``循環(huán)體之內(nèi)的話(huà),就需要額外的添加退出處理,通過(guò)捕獲異常后的break退出當(dāng)前循環(huán)。

@Override
public void run() {
    while (true) {
        try {
            // 執(zhí)行任務(wù)...
        } catch (InterruptedException ie) {  
            // InterruptedException在while(true)循環(huán)體內(nèi)。
            // 當(dāng)線(xiàn)程產(chǎn)生了InterruptedException異常時(shí),while(true)仍能繼續(xù)運(yùn)行!需要手動(dòng)退出
            break;
        }
    }
}
  • 終止運(yùn)行線(xiàn)程

通常,我們通過(guò)“標(biāo)記”方式終止處于“運(yùn)行狀態(tài)”的線(xiàn)程。其中,包括“中斷標(biāo)記”和“額外添加標(biāo)記”。通過(guò)設(shè)立一個(gè)標(biāo)志來(lái)在線(xiàn)程運(yùn)行的時(shí)候判斷是否執(zhí)行下去。

@Override
public void run() {
    while (!isInterrupted()) {
    }
}

isInterruptedThread的內(nèi)部方法,可以獲取當(dāng)前線(xiàn)程是否中斷的標(biāo)志,當(dāng)線(xiàn)程處于運(yùn)行狀態(tài)時(shí),我們通過(guò)interrupt()修改線(xiàn)程的中斷標(biāo)志,來(lái)達(dá)到退出while循環(huán)的作用。

上述是系統(tǒng)內(nèi)部的標(biāo)志符號(hào),我們也可以自己設(shè)置一個(gè)標(biāo)志符來(lái)達(dá)到退出線(xiàn)程的作用

private volatile boolean isExit= false;
protected void exitThread() {
    isExit= true;
}

@Override
public void run() {
    while (isExit) {
    }
}

通過(guò)自己設(shè)置標(biāo)志符,在需要的時(shí)候直接調(diào)用exitThread就可以修改while的判斷條件,從而達(dá)到退出線(xiàn)程的目的。

綜合阻塞和運(yùn)行狀態(tài)下線(xiàn)程的終止方式,結(jié)合兩者可以使用一個(gè)通用較為安全的方法

@Override
public void run() {
    try {
        // 1. isInterrupted()保證,只要中斷標(biāo)記為true就終止線(xiàn)程。
        while (!isInterrupted()) {
        }
    } catch (InterruptedException ie) {  
        // 2. InterruptedException異常保證,當(dāng)InterruptedException異常產(chǎn)生時(shí),線(xiàn)程被終止。
    }
}

最后談?wù)?interrupted()isInterrupted()。
interrupted()isInterrupted()都能夠用于檢測(cè)對(duì)象的“中斷標(biāo)記”。
區(qū)別是,interrupted()除了返回中斷標(biāo)記之外,它還會(huì)清除中斷標(biāo)記(即將中斷標(biāo)記設(shè)為false);而isInterrupted()僅僅返回中斷標(biāo)記。

  • Sleep

    最后簡(jiǎn)單說(shuō)一下sleep,這算是多線(xiàn)程我們最常用的方法了

    sleep是Thread的靜態(tài)native方法,它的作用是讓當(dāng)前線(xiàn)程按照指定的時(shí)間休眠,休眠時(shí)期線(xiàn)程不會(huì)釋放鎖,但是會(huì)讓出執(zhí)行當(dāng)前線(xiàn)程的cpu資源給其他線(xiàn)程使用,和wait較為類(lèi)似,但是也有一些不同點(diǎn)。

    • sleep()是Thread的靜態(tài)內(nèi)部方法,wait()是object類(lèi)的方法
    • wait()方法必須在同步代碼塊中使用,必須獲得對(duì)象鎖(monitor),sleep()方法則可以再仍和地方中使用,wait()方法會(huì)釋放當(dāng)前占有的對(duì)象鎖,本身進(jìn)入waitset隊(duì)列,等待被喚醒,sleep()方法只會(huì)讓出cpu資源,并不會(huì)釋放鎖
    • sleep()方法在休眠時(shí)間結(jié)束后獲得CPU分配的資源后就可以繼續(xù)執(zhí)行,wait()方法需要被notify()喚醒后還需要等待喚醒線(xiàn)程執(zhí)行完畢釋放鎖后,才會(huì)獲得CPU資源繼續(xù)執(zhí)行

線(xiàn)程的狀態(tài)轉(zhuǎn)換以及基本操作

Java 并發(fā)編程:線(xiàn)程間的協(xié)作

Java多線(xiàn)程系列--“基礎(chǔ)篇”09之 interrupt()和線(xiàn)程終止方式

?著作權(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)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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