線程間通訊

我是阿福,公衆號 JavaClub做者,一個在後端技術路上摸盤滾打的程序員,在進階的路上,共勉!
文章已收錄在 JavaSharing 中,包含Java技術文章,面試指南,資源分享。

掌握的技術點以下:java

  • 使用wait/notify實現線程間的通訊
  • 線程的生命週期
  • 生產者/消費者模式的實現
  • 方法join的使用
  • ThreadLocal類的使用

線程間通訊

3.1 使用wait/notify實現線程間的通訊

3.1.1 等待/通知機制的實現

什麼是等待/通知機制git

等待/通知機制在咱們生活中比比皆是,好比在就餐時就會出現,以下圖所示:
程序員

  • 廚師作完一道菜的時間不肯定,因此廚師將菜品放到「菜品傳遞臺」上的時間也不肯定。
  • 服務員取到菜的時間取決於廚師,因此服務員就有「等待」(wait)的狀態。
  • 廚師將菜放到「菜品傳遞臺」上,其實就至關於一種通知(notify),這是服務員才能拿到菜交給就餐者。

這個過程就出現了「等待/通知」機制。github

使用專業術語講面試

等待/通知機制,是指線程A調用了對象O的wait()方法進入等待狀態,而線程B調用了對象O的notify()/notifyAll()方法,線程A收到通知後退出等待隊列,進入可運行狀態,進而執行後續操做。上述兩個線程經過對象O來完成交互,而對象上的wait()方法和notify()/notifyAll()方法的關係就如同開關信號同樣,用來完成等待方和通知方之間的交互工做。編程

等待/通知機制的實現後端

wait()方法的做用多線程

是使當前線程進入阻塞狀態,同時在調用wait()方法以前線程必須得到該對象的對象級別鎖,即只能在同步方法或同步代碼塊中調用wait()方法。在執行wait()方法以後,當前線程釋放鎖併發

notify() notifyAll()方法的做用:dom

就是用來通知那些等待該對象的對象鎖的其餘線程,若是有多個線程等待,則由線程規劃器隨機挑選其中一個呈wait()狀態的線程,對其發起通知notify,並使它獲取該對象的對象鎖。

須要說明的是:在執行notify()方法以後,當前線程不會立刻釋放該對象鎖,呈wait()狀態的線程也不能立刻獲取該對象鎖,要等到執行notify()方法的線程將程序執行完,也就是退出synchronized
代碼塊,當前線程纔會釋放鎖,而呈wait()狀態所在的線程才能夠獲取對象鎖。

強調notify(),notifyAll()也是在同步方法或者是同步代碼塊中調用,即在調用以前必須得到該對象的對象級別鎖

用一句話總結一下wait和notify: wait使線程中止運行,而notify使中止的線程繼續運行

下面代碼實現一個示例:

建立MyList.java,代碼以下:

public class MyList {
    private static List list=new ArrayList();
    public static void add(){
        list.add("anyString");
    }
    public static int size(){
        return list.size();
    }
}

自定義線程類 MyThread1.java, MyThread2.javaMyThread3.java代碼以下:

public class MyThread1 extends Thread {
    private Object lock;

    public MyThread1(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        try {
            synchronized (lock) {
                if (MyList.size() != 5) {
                    System.out.println("開始 wait time=" + System.currentTimeMillis());
                    lock.wait();
                    System.out.println("結束 wait time=" + System.currentTimeMillis());
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

public class MyThread2 extends Thread {

    private Object lock;

    public MyThread2(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock) {
            for (int i = 0; i < 10; i++) {
                MyList.add();
                if (MyList.size() == 5) {
                    lock.notify();
                    System.out.println(" 已發出通知");
                }
                System.out.println("添加了" + (i + 1) + " 個元素!!");
            }
        }
    }
}

建立測試類 Test.java

public class Test {
    public static void main(String[] args) {
       Object lock=new Object();
       MyThread1 myThread1=new MyThread1(lock);
       myThread1.start();
       MyThread2 myThread2=new MyThread2(lock);
       myThread2.start();
    }
}

程序代碼運行結果以下:

開始 wait time=1618832467129
添加了1 個元素!!
添加了2 個元素!!
添加了3 個元素!!
添加了4 個元素!!
已發出通知
添加了5 個元素!!
添加了6 個元素!!
添加了7 個元素!!
添加了8 個元素!!
添加了9 個元素!!
添加了10 個元素!!
結束 wait time=1618832467130

從運行的結果來看,這也說明notify()方法執行後不是當即釋放鎖。


3.2 線程的生命週期

線程生命週期轉換圖

線程的狀態

線程從建立,運行到結束老是處於五種狀態之一:新建狀態,就緒狀態,運行狀態,阻塞狀態,死亡狀態。
  • 新建狀態 :線程對象被建立後就進入了新建狀態,Thread thread = new Thread();
  • 就緒狀態(Runnable):也被稱之爲「可執行狀態」,當線程被new出來後,其餘的線程調用了該對象的start()方法,即thread.start(),此時線程位於「可運行線程池」中,只等待獲取CPU的使用權,隨時能夠被CPU調用。進入就緒狀態的進程除CPU以外,其餘運行所需的資源都已經所有得到。
  • 運行狀態(Running):線程獲取CPU權限開始執行。注意:線程只能從就緒狀態進入到運行狀態。
  • 阻塞狀態(Bloacked):阻塞狀態是線程由於某種緣由放棄CPU的使用權,暫時中止運行,知道線程進入就緒狀態後纔能有機會轉到運行狀態。

阻塞的狀況分三種:

(1)、等待阻塞:運行的線程執行wait()方法,該線程會釋放佔用的全部資源,JVM會把該線程放入「等待池中」。進入這個狀態後是不能自動喚醒的,必須依靠其餘線程調用notify()或者notifyAll()方法才能被喚醒。
(2)、同步阻塞:運行的線程在獲取對象的(synchronized)同步鎖時,若該同步鎖被其餘線程佔用,則JVM會吧該線程放入「鎖池」中。

(3)、其餘阻塞:經過調用線程的sleep()或者join()或發出了I/O請求時,線程會進入到阻塞狀態。當 sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程從新回到就緒狀態。

  • 死亡狀態(Dead):線程執行完成或者因異常退出run方法,該線程結束生命週期。

阻塞線程方法的說明:

  • wait(), notify(),notifyAll()這三個方法是結合使用的,都屬於Object中的方法,wait的做用是使當前線程釋放它所持有的鎖進入等待狀態(釋放對象鎖),而notify和notifyAll則是喚醒當前對象上的等待線程。
  • sleep() 和 yield()方法是屬於Thread類中的sleep()的做用是讓當前線程休眠(正在執行的線程主動讓出CPU,而後CPU就能夠去執行其餘任務),即當前線程會從「運行狀態」進入到阻塞狀態」,但仍然保持對象鎖。當延時時間事後該線程從新阻塞狀態變成就緒狀態,從而等待CPU的調度執行。
  • yield()的做用是讓步,它可以讓當前線程從運行狀態進入到就緒狀態」,從而讓其餘等待線程獲取執行權,可是不能保證在當前線程調用yield()以後,其餘線程就必定能得到執行權,也有多是當前線程又回到「運行狀態」繼續運行。

wait () , sleep()的區別:

一、 sleep()睡眠時,保持對象鎖,仍然佔有該鎖,而wait()釋放對象鎖.
二、 wait只能在同步方法和同步代碼塊裏面使用,而sleep能夠在任何地方使用。
三、 sleep必須捕獲異常,而wait不須要捕獲異常


3.3 生產者/消費者模式的實現

生產者消費者問題(Producer-consumer problem),也稱有限緩衝問題(Bounded-buffer problem),是一個多線程同步問題的經典案例。生產者生成必定量的數據放到緩衝區中,而後重複此過程;與此同時,消費者也在緩衝區消耗這些數據。生產者和消費者之間必須保持同步,要保證生產者不會在緩衝區滿時放入數據,消費者也不會在緩衝區空時消耗數據。不夠完善的解決方法容易出現死鎖的狀況,此時進程都在等待喚醒。

解決生產者/消費者問題的方法可分爲兩類

(1)採用某種機制保護生產者和消費者之間的同步;
(2)在生產者和消費者之間創建一個管道。第一種方式有較高的效率,而且易於實現,代碼的可控制性較好,屬於經常使用的模式。第二種管道緩衝區不易控制,被傳輸數據對象不易於封裝等,實用性不強。所以本文只介紹同步機制實現的生產者/消費者問題。

同步問題核心在於

如何保證同一資源被多個線程併發訪問時的完整性。經常使用的同步方法是採用信號或加鎖機制,保證資源在任意時刻至多被一個線程訪問。Java語言在多線程編程上實現了徹底對象化,提供了對同步機制的良好支持。在Java中一共有四種方法支持同步,其中前三個是同步方法,一個是管道方法。

(1)wait() / notify()方法

(2)await() / signal()方法

(3)BlockingQueue阻塞隊列方法

(4)PipedInputStream / PipedOutputStream

下面咱們經過 wait() / notify()方法實現生產者和消費者模式:

代碼場景:

當緩衝區已滿時,生產者線程中止執行,放棄鎖,使本身處於等狀態,讓其餘線程執行;
當緩衝區已空時,消費者線程中止執行,放棄鎖,使本身處於等狀態,讓其餘線程執行。

當生產者向緩衝區放入一個產品時,向其餘等待的線程發出可執行的通知,同時放棄鎖,使本身處於等待狀態;
當消費者從緩衝區取出一個產品時,向其餘等待的線程發出可執行的通知,同時放棄鎖,使本身處於等待狀態。

代碼實現:

建立倉庫Storage.java 代碼:

public class Storage {
    // 倉庫容量
    private final int MAX_SIZE = 10;
    // 倉庫存儲的載體
    private LinkedList<Object> list = new LinkedList<>();

    public void produce() {
        synchronized (list) {
            while (list.size() + 1 > MAX_SIZE) {
                System.out.println("【生產者" + Thread.currentThread().getName()
                        + "】倉庫已滿");
                try {
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            list.add(new Object());
            System.out.println("【生產者" + Thread.currentThread().getName()
                    + "】生產一個產品,現庫存" + list.size());
            list.notifyAll();
        }
    }

    public void consume() {
        synchronized (list) {
            while (list.size() == 0) {
                System.out.println("【消費者" + Thread.currentThread().getName()
                        + "】倉庫爲空");
                try {
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            list.remove();
            System.out.println("【消費者" + Thread.currentThread().getName()
                    + "】消費一個產品,現庫存" + list.size());
            list.notifyAll();
        }
    }

}

建立生產者線程Producer.java,消費者線程Consumer.java,代碼以下:

public class Producer implements Runnable {

    private Storage storage;

    public Producer(){}

    public Producer(Storage storage){
        this.storage = storage;
    }

    @Override
    public void run(){
        while(true){
            storage.produce();
        }
    }

}
public class Consumer implements Runnable{
    private Storage storage;

    public Consumer(){}

    public Consumer(Storage storage){
        this.storage = storage;
    }

    @Override
    public void run(){
        while(true){
            storage.consume();
        }
    }

}

建立測試類TestPc.java

public class TestPc {

    public static void main(String[] args) {
        Storage storage = new Storage();
        Thread p1 = new Thread(new Producer(storage));
        p1.setName("張三");
        p1.start();
        Thread c1=new Thread(new Consumer(storage));
        c1.start();
        c1.setName("李四");

    }


}

程序運行的部分結果:

【消費者李四】消費一個產品,現庫存8
【消費者李四】消費一個產品,現庫存7
【消費者李四】消費一個產品,現庫存6
【消費者李四】消費一個產品,現庫存5
【消費者李四】消費一個產品,現庫存4
【生產者張三】生產一個產品,現庫存5
【生產者張三】生產一個產品,現庫存6
【生產者張三】生產一個產品,現庫存7
【生產者張三】生產一個產品,現庫存8
【生產者張三】生產一個產品,現庫存9
【生產者張三】生產一個產品,現庫存10
【生產者張三】倉庫已滿
【消費者李四】消費一個產品,現庫存9
【消費者李四】消費一個產品,現庫存8
【消費者李四】消費一個產品,現庫存7

3.4 方法join的使用

在不少狀況下,主線程建立並啓動子線程,若是子線程中進行大量的運算,主線程每每早於子線程結束。這時主線程要等待子線程完成以後再結束。好比子線程處理一個數據,主線程要取得這個數據中的值,就要用到join()方法。

join()方法就是等待線程對象銷燬。

建立測試MyJoinThread.java代碼:

public class MyJoinThread extends Thread{

    @Override
    public void run() {
        int secondValue= (int) (Math.random() * 10000);
        System.out.println(secondValue);
        try {
            Thread.sleep(secondValue);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

建立測試類TestJoin.java代碼:

public class TestJoin {
    public static void main(String[] args) throws InterruptedException {
        MyJoinThread myJoinThread=new MyJoinThread();
        myJoinThread.start();
        //myJoinThread.join();
        System.out.println("我想當myJoinThread對象執行完畢我再執行,答案是不肯定的");
    }
}

代碼的運行結果:

我想當myJoinThread對象執行完畢我再執行,答案是不肯定的
9618

把myJoinThread.join()代碼註釋去掉運行代碼執行結果以下:

82
我想當myJoinThread對象執行完畢我再執行,答案是不肯定的

因此得出結論是:join()方法使所屬線程對象myJoinThread正常執行run()方法中的任務,而使當前線程main進行無限的阻塞,等待myJoinThread銷燬完再繼續執行main線程後面的代碼。

方法join()具備使線程排隊運行的做用,有點相似同步運行的效果。

join()和synchronized的區別是:join()在內部使用wait()方法進行等待,而synchronized關鍵字使用的是「對象監聽器」的原理作的同步。

方法join()與sleep(long)的區別

方法join(long)的功能在內部使用的是wait(long)方法實現的,所用join(long)方法具備釋放鎖的特色。

方法join(long)源代碼以下:

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;
            }
        }
    }

從源代碼中能夠了解到,當執行wait(long)方法後,當前線程的鎖被釋放,那麼其餘線程能夠調用此線程中的同步方法了。

而Thread.sleep(long)方法不釋放鎖。


3.5 ThreadLocal類的使用

咱們知道變量值的共享可使用public static 變量的形式,若是想實現每個線程都有本身的共享變量該如何解決呢? JDK中提供ThreadLocal正是解決這樣的問題。

類ThreadLocal主要解決的就是爲每一個線程綁定本身的值,能夠將ThreadLocal類比喻成全局存放數據的盒子,盒子中能夠存儲每個線程的私有數據。

建立run.java類,代碼以下:

public class run {
  private static  ThreadLocal threadLocal=new ThreadLocal();

    public static void main(String[] args) {
       if (threadLocal.get()==null){
           System.out.println("從未放過值");
           threadLocal.set("個人值");
       }
        System.out.println(Thread.currentThread().getName()+"線程:"+threadLocal.get());
    }
    

}

代碼的運行結果:

從未放過值
main線程:個人值

從圖中運行結果來看,第一次調用threadLocal對象的get方法返回爲null,經過調用set()賦值後值打印在控制檯上,類ThreadLocal解決的是變量在不一樣線程間的隔離性,也就是不一樣的線程擁有本身的值,不一樣線程的值能夠存放在ThreadLocal類中進行保存的。

驗證線程變量的隔離性

建立ThreadLocalTest項目,類 Tools.java代碼以下:

public class Tools {
    public static ThreadLocal local=new ThreadLocal();
}

建立線程類 MyThread1.java ,MyThread2.java代碼以下:

public class MyThread1 extends Thread {

    @Override
    public void run() {
        for (int j = 0; j < 5; j++) {
            Tools.local.set(j+1);
            System.out.println(Thread.currentThread().getName()+"get value:"+Tools.local.get());
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class MyThread2 extends Thread {


    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            Tools.local.set(i+1);
            System.out.println(Thread.currentThread().getName()+"get value:"+Tools.local.get());
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

建立run.java 測試類

public class run {

    public static void main(String[] args) {
       MyThread1 myThread1=new MyThread1();
       myThread1.setName("myThread1線程");
       myThread1.start();
       MyThread2 myThread2=new MyThread2();
       myThread2.setName("myThread2線程");
       myThread2.start();
    }
}

程序運行結果:

myThread1線程get value:1
myThread2線程get value:1
myThread2線程get value:2
myThread1線程get value:2
myThread1線程get value:3
myThread2線程get value:3
myThread2線程get value:4
myThread1線程get value:4
myThread1線程get value:5
myThread2線程get value:5

雖然2個線程都向local中set()數據值,但每一個線程仍是能取到本身的數據。


文章參考:

《Java多線程編程核心技術》
https://blog.csdn.net/ldx1998...
https://blog.csdn.net/MONKEY_...

看到這裏今天的分享就結束了,若是以爲這篇文章還不錯,來個分享、點贊、在看三連吧,讓更多的人也看到~

歡迎關注我的公衆號 「JavaClub」,按期爲你分享一些技術乾貨。

相關文章
相關標籤/搜索