多線程基礎知識

​本節內容:java

線程的狀態

wait/notify/notifyAll/sleep方法的介紹

如何正確中止線程

有哪些實現生產者消費者的方法

<span id="jump1">線程的狀態/span>

線程一共有六種狀態,分別是New(新建)、Runnable(可運行)、Blocked(阻塞)、Waiting(等待)、Timed WaitIng(計時等待)、Terminated(終結) 設計模式

狀態流轉圖安全

多線程基礎知識

NEW(新建) 多線程

當咱們new一個新線程的時候,若是還未調用start()方法,則該線程的狀態就是NEW,而一旦調用了start()方法,它就會從NEW變成Runnableide

Runnable(可運行)this

java中的可運行狀態分爲兩種,一種是可運行,一種是運行中,若是當前線程調用了start()方法以後,還未獲取CPU時間片,此時該線程處於可運行狀態,等待被分配CPU資源,若是得到CPU資源後,該線程就是運行狀態。spa

Blocked(阻塞) 線程

java中的阻塞也分三種狀態:Blocked(被阻塞)、Waiting(等待)、Timed Waiting(計時等待),這三種狀態統稱爲阻塞狀態。設計

  • Blocked狀態(被阻塞):從結合圖中能夠看出從Runnable狀態進入Blocked狀態只有進入synchronized保護的代碼時,沒有獲取到鎖monitor鎖,就會處於Blocked狀態code

  • Time Waiting(計時等待):Time Waiting和Waiting狀態的區別是有沒有時間的限制,一下狀況會進入Time Waiting:

  • 設置了時間參數的Thread.sleep(long millis)

  • 設置了時間參數的Object.wait(long timeout)

  • 設置了時間參數的Thread.join(long millis)

  • 設置了時間參數的LockSupport.parkNanos(long millis)和LockSupport.parkUntil(long deadline)

  • Waiting狀態(等待):線程進入Waiting狀態有三種狀況,分別是:

  • 沒有設置Timeout的Object.wait()方法

  • 沒有設置Timeout的Thread.join()方法

  • LockSupport.park()方法

Blocked狀態僅僅針對synchronized monitor鎖,若是獲取的鎖是ReentrantLock等鎖時,線程沒有搶到鎖就會進入Waiting狀態,由於本質上它執行的是LockSupport.park()方法,因此會進入Waiting方法,一樣Object.wait()、Thread.join()也會讓線程進入waiting狀態。Blocked和Waiting不一樣的是blocked等待其餘線程釋放monitor鎖,而Waiting則是等待某個條件,相似join線程執行完畢或者notify()\notifyAll()。

上圖中能夠看出處於Waiting、Time Waiting的線程調用notify()或者notifyAll()方法後,並不會進入Runnable狀態而是進入Blocked狀態,由於喚醒處於Waiting、Time Waiting狀態的線程的線程在調用notify()或者notifyAll()時候,必須持有該monitor鎖,因此處於Waiting、Time Waiting狀態的線程被喚醒後,就會進入Blocked狀態,直到執行了notify()\notifyAll()的線程釋放了鎖,被喚醒的線程才能夠去搶奪這把鎖,若是搶到了就從Blocked狀態轉換到Runnable狀態

Terminated(終結)

進入這個狀態的線程分兩種狀況:

  1. run()方法執行完畢,正常退出

  2. 發生異常,終止了run()方法。

<span id="jump2">wait/notify/notifyAll方法的使用</span>

首先wait方法必須在sychronized保護的同步代碼中使用,在wait方法的源碼註釋中就有說:

在使用wait方法是必須把wait方法寫在synchronized保護的while代碼中,而且始終判斷執行條件是否知足,若是知足就繼續往下執行,不知足就執行wait方法,並且執行wait方法前,必須先持有對象的synchronized鎖.

上面主要是兩點:

  1. wait方法要在synchronized同步代碼中調用.

  2. wait方法應該老是被調用在一個循環中

咱們先分析第一點,結合如下場景分析爲何要這麼設計

public class TestDemo {
private ArrayBlockingQueue<String> storage = new ArrayBlockingQueue(8);

public void add(String data){
        storage.add(data);
        notify();
    }

public String remove() throws InterruptedException {
//wait不用synchronized關鍵字保護,直接調用,
while (storage.isEmpty()){
            wait();
        }
return storage.remove();
    }
}

上述代碼是一個簡單的基於ArrayBlockingQueue實現的生產者、消費者模式,生產者調用add(String data)方法向storage中添加數據,消費者調用remove()方法從storage中消費數據.

代碼中咱們能夠看到若是wait方法的調用沒有用synchronized保護起來,那麼就可能發生一下場景狀況:

  1. 消費者線程調用remove()方法判斷storage是否爲空,若是是就調用wait方法,消費者線程進入等待,可是這就可能發生消費者線程調用完storage.isEmpty()方法後就被調度器暫停了,而後還沒來得及執行wait方法.

  2. 此時生產者線程開始運行,開始執行了add(data)方法,成功的添加了data數據而且執行了notify()方法,可是由於以前的消費者尚未執行wait方法,因此此時沒有線程被喚醒.

  3. 生產者執行完畢後,剛纔被調度器暫停的消費者再回來執行wait方法,而且進入了等待,此時storage中已經有數據了.

以上的狀況就是線程不安全的,由於wait方法的調用錯過了notify方法的喚醒,致使應該被喚醒的線程沒法收到notify方法的喚醒.

正是由於wait方法的調用沒有被synchronized關鍵字保護,因此他和while判斷不是原子操做,因此就會出現線程安全問題.

咱們把以上代碼改爲以下,就實現了線程安全

public class TestDemo {
private ArrayBlockingQueue<String> storage = new ArrayBlockingQueue(8);

public void add(String data){
synchronized (this){
            storage.add(data);
            notify();
        }
    }

public String remove() throws InterruptedException {
synchronized (this){
while (storage.isEmpty()){
                wait();
            }
return storage.remove();
        }
    }
}

咱們再來分析第二點wait方法應該老是被調用在一個循環中?

之因此將wait方法放到循環中是爲了防止線程「虛假喚醒「(spurious wakeup),線程可能在沒有被notify/notyfiAll,也沒有被中斷或者超時的狀況下被喚醒,雖然這種機率發生很是小,可是爲了保證發生虛假喚醒的正確性,因此須要採用循環結構,這樣即使線程被虛假喚醒了,也會再次檢查while的條件是否知足,不知足就調用wait方法等待.

爲何wait/notify/notifyAll被定義在Object類中

java中每一個對象都是一個內置鎖,都持有一把稱爲monitor監視器的鎖,這就要求在對象頭中有一個用來保存鎖信息的位置.這個鎖是對象級別的而非線程級別的,wait/notify/notifyAll也都是鎖級別的操做,它們的鎖屬於對象,因此把它們定義在Object中最合適.

wait/notify和sleep方法的異同

相同點:

  1. 它們均可以讓線程阻塞

  2. 它們均可以響應interrupt中斷:在等待過程當中若是收到中斷信號,均可以進行響應並拋出InterruptedException異常

不一樣點:

  1. wait方法必須在synchronized同步代碼中調用,sleep方法沒有這個要求

  2. 調用sleep不會釋放monitor鎖,調用wait方法就釋放monitor鎖

  3. sleep要求等待一段時間後會自動恢復,可是wait方法沒有設置超時時間的話會一直等待,直到被中斷或者被喚醒,不然不能主動恢復

  4. wait/notify是Object方法,sleep是Thread的方法

<span id="jump3">如何正確中止線程</span>

正確的中止線程方式是經過使用interrupt方法,interrupt方法僅僅起到了通知須要被中斷的線程的做用,被中斷的線程有徹底的自主權,它能夠馬上中止,也能夠執行一段時間再中止,或者壓根不中止.這是由於java但願程序之間能互相通知、協做的完成任務.

interrupt()方法的使用

public class InterruptDemo implements Runnable{

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new InterruptDemo());
        thread.start();
        Thread.sleep(5);
        thread.interrupt();
    }

    @Override
    public void run() {
        int i =0;
        while (!Thread.currentThread().isInterrupted() && i<1000){
            System.out.println(i++);
        }
    }
}

上圖中經過循環打印0~999,可是實際運行並不會打印到999,由於在線程打印到999以前,咱們對線程調用了interrupt方法使其中斷了,而後根據while中的判斷條件,方法提早終止,運行結果以下:

多線程基礎知識

其中若是是經過sleep、wait方法使線程陷入休眠,處於休眠期間的線程若是被中斷是能夠感覺到中斷信號的,而且會拋出一個InterruptException異常,同時清除中斷信號,將中斷標記位設置爲false.

<span id="jump3">有哪些實現生產者消費者的方法</span>

生產者消費者模式是程序設計中常見的一種設計模式,咱們經過下圖來理解生產者消費者模式:

使用BolckingQueue實現生產者消費者模式

經過利用阻塞隊列ArrayBlockingQueue實現一個簡單的生產者消費者模式,建立兩個線程用來生產對象,兩個線程用來消費對象,若是ArrayBlockingQueue滿了,那麼生產者就會阻塞,若是ArrayBlockingQueue爲空,那麼消費者線程就會阻塞.線程的阻塞和喚醒都是經過ArrayBlockingQueue來完成的.

public void MyBlockingQueue1(){
        BlockingQueue<Object> queue=new ArrayBlockingQueue<>(10);
        Runnable producer = () ->{
            while (true){
                try {
                    queue.put(new Object());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        new Thread(producer).start();
        new Thread(producer).start();

        Runnable consumer = () ->{
            while (true){
                try {
                    queue.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        new Thread(consumer).start();
        new Thread(consumer).start();
    }

使用Condition實現生產者消費者模式

以下代碼其實也是相似ArrayBlockingQueue內部的實現原理.

以下代碼所示,定義了一個隊列容量是16的的queue,用來存放數據,定義一個ReentrantLock類型的鎖,並在Lock鎖的基礎上建立了兩個Condition,一個是notEmpty一個是notFull,分別表明隊列沒有空和沒有滿的條件,而後就是put和take方法.

put方法中,由於是多線程訪問環境,因此先上鎖,而後在while條件中判斷queue中是否已經滿了,若是滿了,則調用notFull的await()方法阻塞生產者並釋放Lock鎖,若是沒有滿則往隊列中放入數據,而且調用notEmpty.singleAll()方法喚醒全部的消費者線程,最後在finally中釋放鎖.

同理take方法和put方法相似,一樣是先上鎖,在判斷while條件是否知足,而後執行對應的操做,最後在finally中釋放鎖.

public class MyBlockingQueue2 {
    private Queue queue;
    private int max;
    private ReentrantLock lock=new ReentrantLock();
    private Condition notEmpty = lock.newCondition();
    private Condition notFull =lock.newCondition();

    public MyBlockingQueue2(int size){
        this.max =size;
        queue = new LinkedList();
    }

    public void put(Object o) throws InterruptedException {
        lock.lock();
        try {
            while (queue.size() == max) {
                notFull.await();
            }
            queue.add(o);
            //喚醒全部的消費者
            notEmpty.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public Object take() throws InterruptedException{
        lock.lock();
        try {
        //這裏不能改用if判斷,由於生產者喚醒了全部的消費者,
        //消費者喚醒後,必須在進行一次條件判斷
            while (queue.size() == 0) {
                notEmpty.await();
            }
            Object remove = queue.remove();
            //喚醒全部的生產者
            notFull.signalAll();
            return remove;
        }finally {
            lock.unlock();
        }
    }
}

使用wait/notify實現生產者消費者模式

以下代碼所示,利用wait/notify實現生產者消費者模式主要是在put和take方法上加了synchronized鎖,而且在各自的while方法中進行條件判斷

public class MyBlockingQueue3 {
    private int max;
    private Queue<Object> queue;

    public MyBlockingQueue3(int size){
        this.max =size;
        this.queue=new LinkedList<>();
    }

    public synchronized void put(Object o) throws InterruptedException {
        while(queue.size() == max){
            wait();
        }
        queue.add(o);
        notifyAll();
    }

    public synchronized Object take() throws InterruptedException {
        while (queue.size() == 0){
            wait();
        }
        Object remove = queue.remove();
        notifyAll();
        return remove;
    }
}

以上就是三種實現生產者消費者模式的方式,第一種比較簡單直接利用ArrayBlockingQueue內部的特徵完成生產者消費者模式的實現場景,第二種是第一種背後的實現原理,第三種利用synchronzied實現.

相關文章
相關標籤/搜索