多線程筆記---線程間的協做方法(wait、notify、sleep、yield、join、interrupt、notifyAll)

線程的狀態

萬事萬物都有其本身的生命週期和狀態,一個線程從建立到結束被銷燬也有其本身的六種狀態,而wait、notify、sleep等等這些方法就是協助切換線程間的狀態html

Oracle官方文檔提供的六種線程狀態java

狀態名稱 說明
NEW 初始狀態,線程被建立,可是尚未調用start()方法,線程還未被啓動
RUNNABLE 運行狀態,一個線程開始在java虛擬機中被執行
BLOCKED 阻塞狀態,線程被鎖住等待得到對象的monitor lock,換言之就是被鎖(Synchronize)阻塞了
WAITING 等待狀態,無限期等待另外一個線程執行特定操做的線程處於此狀態。
TIMED_WAITING 超時等待狀態,在指定的等待時間內等待另外一個線程執行操做的線程處於此狀態。
TERMINATED 終止狀態,線程執行完畢已經退出

用一張圖能夠清晰的表示上述狀態在線程中的運行狀態切換 面試

《JAVA併發編程的藝術》一書中的線程狀態轉換圖

線程的狀態切換的操做

創建線程後咱們會根據需求對線程進行一些操做,這些操做會改變線程的基本狀態,同事也成爲了線程間的一種通訊方式,下面就主要聊聊這些方法。編程

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

    wait方法主要是將當前運行的線程掛起,讓其進入阻塞狀態,而後釋放它持有的同步鎖(也就是前面文章提到的monitor),通知其餘線程來獲取執行,直到notifynotifyAll方法來喚醒。api

    wait也是一個多參數方法,能夠經過wait(long timeout)來設定線程在指定時間內若是沒有notifynotifyAll方法的喚醒,也會自動喚醒,wait方法調用的也是這個方法,不過傳入的參數爲0L。安全

    在使用wait方法時,必定要在同步範圍內,不然就會拋出IllegalMonitorStateException異常。bash

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());
    }
}
運行結果:
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文檔對於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.

複製代碼

該錯誤的大意爲:線程試圖等待一個對象的監視器或者去通知其餘在等待對象監視器的線程,可是該線程自己沒有持有指定的監視器.主要是由於調用wait方法時沒有獲取到對象的monitor,得到的途徑能夠經過Synchronized關鍵字來完成,在上述代碼的方法中添加Synchronized關鍵字併發

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());
    }
複製代碼

經過這個例子得知,wait方法的使用必須在同步的範圍內,不然就會拋出IllegalMonitorStateException異常,wait方法的做用就是阻塞當前線程等待notify/notifyAll方法的喚醒,或等待超時後自動喚醒。oracle

wait方法經過釋放對象的monitor來掛起線程,進入WaitSet隊列, 而後後續等待鎖線程繼續來執行,直到同一對象上調用notifynotifyAll後才能夠喚醒等待線程。

notify 和 notifyAll的區別是notify方法只喚醒一個等待(對象的)線程並使該線程開始執行,若是有多個線程等待一個對象,那麼只會隨機喚醒其中一個線程,後者則會喚醒全部等待(對象的)線程,哪一個線程第一個被喚醒也是取決於操做系統。

負責調用方法去喚醒線程的線程也被稱爲喚醒線程,喚醒線程後不能被馬上執行,由於喚醒線程還持有該對象的同步鎖,必須等待喚醒線程執行完畢後釋放了對象的同步鎖後,等待線程才能獲取到對象的同步鎖進而繼續執行。

從上述中能夠看到wait,notify,notifyAll方法的調用去掛起喚醒線程主要是操做對象的monitor,而monitor是全部對象的對象頭裏都擁有的,因此這三個方法定義在Object類中,而不是Thread類中

下面一個用經典面試題:雙線程打印奇偶數來展現wait和notify的用法(代碼隨便寫的,理會意思就行)

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

    public Main() {
    }

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

        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; // 打印的類型,0:表明打印奇數,1:表明打印偶數

        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("當前是"+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("當前是"+name+"輸出"+status.get());
                            status.set(status.get()+1);
                            even.notify();
                        }
                    }
                }
            }
        }
    }
}

複製代碼
  • yield

yield是一個靜態的原生native方法,他的做用是讓出當前線程的CPU分配的時間片,將其分配給和當前線程同優先級的線程,而後當前線程狀態由運行中(RUNNING)轉換爲可運行(RUNNABLE)狀態,但這個並非等待或者阻塞狀態,也不會釋放對象鎖,若是在下一次競爭中,又得到了CPU時間片當前線程依然會繼續運行。

如今的操做系統中包含多個進程,一個進程又包含多個線程,那麼這些多線程是一塊兒執行的嗎?就像電腦上,咱們能夠一邊看電視一邊瀏覽網頁,其實並否則,看視兩邊同步進行的,但實際上是cpu讓兩個線程交替執行,只不過交替執行的速度很快,肉眼分辨不出來,因此纔會有同步執行的錯覺。同理,這裏也是同樣,系統會分出一個個時間片,線程會被分配到屬於本身執行的時間片,當前線程的時間片用完後會等待下次分配,線程分配的時間多少也以爲了線程使用多少處理器的資源,線程優先級也就是以爲線程是分配多一些仍是少一些處理器的資源

Java中,經過一個整型變量Priority來控制線程的優先級,範圍爲1~10,經過調用setPriority(int Priority)能夠設置,默認值爲5。

yield同樣,sleep也調用時也會交出當前線程的處理器資源,可是不一樣的是sleep交出的資源全部線程均可以去競爭,yield交出的時間片資源只有和當前線程同優先級的線程才能夠獲取到。

  • join

join方法的做用是父線程(通常是main主線程)等待子線程執行完成後再執行,換言之就是講異步執行的線程合併爲同步的主線程,。

wait同樣,join方法也有多個參數的方法,也能夠設定超時時間,join()方法調用的也是join(0L)

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

    public class MyPrinter implements Runnable {
        String content;

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

        @Override
        public void run() {
            System.out.println("當前線程"+content+"時間:"+System.currentTimeMillis());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
輸出結果:
主線程開始時間:1557824674063
當前線程線程1時間:1557824674063
當前線程線程2時間:1557824675065
主線程結束時間:1557824676065
當前線程線程3時間:1557824676065

複製代碼

從上面例子能夠看到線程1和2調用了join方法後,主線程是等待兩個線程執行完成以後纔會繼續執行

  • interrupt

interrupt的目的是爲了中斷線程,原來Thread.stop, Thread.suspend, Thread.resume 都有這個功能,但因爲都太暴力了而被廢棄了,暴力中斷線程是一種不安全的操做,相對而言interrupt經過設置標誌位的方式就比較溫柔

interrupt基於一個線程不該該由其餘線程來強制中斷或中止,而是應該由線程內部來自行中止的思想來實現的,本身的事本身處理,是一種比較溫柔和安全的作法,並且中斷不活動的線程不會產生任何影響。

從API文檔的中的介紹來看interrupt()的做用是中斷本線程。除非當前線程正在中斷自身(始終容許),不然將調用此線程的checkAccess方法,但這可能致使拋出SecurityException

若是在調用Object類的wait()join()sleep(long)阻塞了這個線程,那麼它的中斷狀態將被清除並收到InterruptedException

若是在InterruptibleChannel上的I / O操做中阻塞了該線程,則該通道將被關閉,線程的中斷狀態將被設置,而且線程將收到ClosedByInterruptException

  • 終止阻塞線程

例如,線程經過wait()進入阻塞狀態,此時經過interrupt()中斷該線程;調用interrupt()會當即將線程的中斷標記設爲「true」,可是因爲線程處於阻塞狀態,因此該「中斷標記」會當即被清除爲「false」,同時,會產生一個InterruptedException的異常。此時將InterruptedException放在適當的位置進行捕獲就能終止阻塞中的線程,以下代碼,將中斷的捕獲放在while(true)以外,就能夠退出while循環

@Override
public void run() {
    try {
        while (true) {
            // 執行任務...
        }
    } catch (InterruptedException ie) {  
        // 因爲產生InterruptedException異常,退出while(true)循環,線程終止!
    }
}
複製代碼

可是若是須要將··InterruptedException··在··while(true)``循環體以內的話,就須要額外的添加退出處理,經過捕獲異常後的break退出當前循環。

@Override
public void run() {
    while (true) {
        try {
            // 執行任務...
        } catch (InterruptedException ie) {  
            // InterruptedException在while(true)循環體內。
            // 當線程產生了InterruptedException異常時,while(true)仍能繼續運行!須要手動退出
            break;
        }
    }
}
複製代碼
  • 終止運行線程

一般,咱們經過「標記」方式終止處於「運行狀態」的線程。其中,包括「中斷標記」和「額外添加標記」。經過設立一個標誌來在線程運行的時候判斷是否執行下去。

@Override
public void run() {
    while (!isInterrupted()) {
    }
}
複製代碼

isInterruptedThread的內部方法,能夠獲取當前線程是否中斷的標誌,當線程處於運行狀態時,咱們經過interrupt()修改線程的中斷標誌,來達到退出while循環的做用。

上述是系統內部的標誌符號,咱們也能夠本身設置一個標誌符來達到退出線程的做用

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

@Override
public void run() {
    while (isExit) {
    }
}
複製代碼

經過本身設置標誌符,在須要的時候直接調用exitThread就能夠修改while的判斷條件,從而達到退出線程的目的。

綜合阻塞和運行狀態下線程的終止方式,結合二者可使用一個通用較爲安全的方法

@Override
public void run() {
    try {
        // 1. isInterrupted()保證,只要中斷標記爲true就終止線程。
        while (!isInterrupted()) {
        }
    } catch (InterruptedException ie) {  
        // 2. InterruptedException異常保證,當InterruptedException異常產生時,線程被終止。
    }
}
複製代碼

最後談談 interrupted()isInterrupted()interrupted()isInterrupted()都可以用於檢測對象的「中斷標記」。 區別是,interrupted()除了返回中斷標記以外,它還會清除中斷標記(即將中斷標記設爲false);而isInterrupted()僅僅返回中斷標記。

  • Sleep

    最後簡單說一下sleep,這算是多線程咱們最經常使用的方法了

    sleep是Thread的靜態native方法,它的做用是讓當前線程按照指定的時間休眠,休眠時期線程不會釋放鎖,可是會讓出執行當前線程的cpu資源給其餘線程使用,和wait較爲相似,可是也有一些不一樣點。

    • sleep()是Thread的靜態內部方法,wait()是object類的方法
    • wait()方法必須在同步代碼塊中使用,必須得到對象鎖(monitor),sleep()方法則能夠再仍和地方中使用,wait()方法會釋放當前佔有的對象鎖,自己進入waitset隊列,等待被喚醒,sleep()方法只會讓出cpu資源,並不會釋放鎖
    • sleep()方法在休眠時間結束後得到CPU分配的資源後就能夠繼續執行,wait()方法須要被notify()喚醒後還須要等待喚醒線程執行完畢釋放鎖後,纔會得到CPU資源繼續執行

線程的狀態轉換以及基本操做
Java 併發編程:線程間的協做
Java多線程系列--「基礎篇」09之 interrupt()和線程終止方式

相關文章
相關標籤/搜索