日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

本文章收錄于《JAVA并發編程》合集中,本篇來介紹線程間通信,線程間通信 使線程成為一個整體,提高系統之間的 交互性,在提高CPU利用率的同時可以對線程任務進行有效的把控與監督。

比如:多線程之間交替執行,多線程按順序執行等,都需要使用線程通信技術,通過本篇文章您可以獲得:

什么是線程通信,有什么作用

線程通信的三種實現方式

notifyAll的虛假喚醒問題,notify死鎖問題

通過 ReentrantLock 實現精確喚醒

多線程按順序執行的四種方案

線程通信常見面試題解析

相信你還有更多方式實現線程通信?不妨評論區告訴我們吧,高頻率碼字不易,覺得文章不錯記得點贊支持一下哦!

線程間通信

線程之間的交互我們稱之為線程通信【Inter-Thread Communication,簡稱ITC】,指多個線程處理同一資源,但是任務不同

比如:小明放假在家,肚子餓了,如果發現沒有吃的就會喊:媽,我餓了,弄點吃的,如果媽媽發現沒有吃的了就會做菜,通知小明吃飯,總之:有菜通知小明吃飯,沒菜小明通知媽媽做飯,簡直吃貨一個

此時就是兩個線程對飯菜這同一資源有不同的任務,媽媽線程就是做飯,小明線程是吃飯,如果想要實現上邊的場景,就需要媽媽線程和小明線程之間通信

要實現線程之間通信一般有三種方法:

  • 使用Object對象鎖的wait()、notify()和notifyAll()方法
  • 使用Java5新增的JUC中的ReentrantLock結合Condition
  • 使用JUC中的CountDownLatch【計數器】

對象鎖wait和notifyAll方法實現

在此案例中,同一資源就是飯菜,小明對吃的操作是造,而媽媽對吃的操作是做

飯菜資源:

public class KitChenRoom {

    // 是否有吃的
    private boolean hasFood = false;
    // 設置同步鎖,做飯和吃飯只能同時有一個在執行,不能邊做邊吃
    private Object lock = new Object();
    // 做飯
    public void cook() {
        // 加鎖
        synchronized (lock) {
            // 如果有吃的,就不做飯
            if(hasFood) {
                // 還有吃的,先不做飯
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            // 否則就做飯,
            System.out.println(Thread.currentThread().getName() + "沒吃的了,給娃做飯!");
            // 做好之后,修改為true
            hasFood = true;
            // 通知其他線程吃飯
            lock.notifyAll();
        }
    }
    
    // 吃飯
    public void eat() {
        synchronized (lock) {
            // 如果沒吃的,就喊媽媽做飯,暫時吃不了
            if (!hasFood) {
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            // 否則就吃飯
            System.out.println(Thread.currentThread().getName() + "感謝老媽,恰飯,恰飯");
            // 吃完之后,修改為false
            hasFood = false;
            // 通知其他線程吃飯
            lock.notifyAll();
        }
    }
}

測試類:

public class KitChenMain {
    public static void main(String[] args) {
                // 創建飯菜對象
        KitChenRoom chenRoom = new KitChenRoom();

        // 創建媽媽線程,做飯
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                chenRoom.cook();
            }
        },"媽媽線程:").start();

        // 創建小明線程,吃飯
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                chenRoom.eat();
            }
        },"小明線程:").start();
    }
}

運行結果:發現兩個線程交替執行,沒飯的時候媽媽做飯,有飯的時候小明就恰飯

虛假喚醒

在wait方法的源碼注釋中有這么一段話:

As in the one argument version, interrupts and spurious wakeups are possible, 
and this method should always be used in a loop

翻譯:在單參數版本中,中斷和虛假喚醒是可能的,并且該方法應始終在循環中使用

比如上邊的 飯菜資源 代碼中我們使用的是if判斷是否有吃的

如果此時我們再開啟一個大明線程吃飯,開啟一個爸爸線程做飯,此時會發生什么問題呢

改造測試類:再開啟一個大明線程和一個爸爸線程

public class KitChenMain {
    public static void main(String[] args) {

        KitChenRoom chenRoom = new KitChenRoom();

        // 創建媽媽線程
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                chenRoom.cook();
            }
        },"媽媽線程:").start();

        // 創建小明線程
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                chenRoom.eat();
            }
        },"小明線程:").start();
        // 爸爸線程:做飯
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                chenRoom.cook();
            }
        },"爸爸線程:").start();

        // 大明線程:吃飯
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                chenRoom.eat();
            }
        },"大明線程:").start();
    }
}

運行結果:發現爸爸線程和媽媽線程連著做了三次飯

原因:

  • 這是由于wait方法的機制導致的,wait方法會使線程阻塞,直到被喚醒之后才會運行,在哪里阻塞,再次被喚醒之后得到CPU執行權,就會在哪里繼續運行
  • 現在是4條線程,假設爸爸線程運行之后將 hasFood 改為true,此時爸爸線程就會喚醒其他線程,也就是媽媽線程和小明,大明線程都會被喚醒,如果此時媽媽線程獲取到CPU時間片開始運行,判斷 hasFood 為 true,那么就觸發wait等待,等待之后就會釋放CPU執行權,喚醒其他線程
  • 如果此時爸爸線程又獲取到CPU執行權,同樣判斷hasFood之后為true,就會進入等待,喚醒其他線程,如果此時CPU執行權又分配給了媽媽線程,因為之前已經經過了判斷,就會在wait的地方,繼續執行,就會觸發給娃做飯,之后再喚醒其他線程
  • 此時爸爸線程得到CPU時間片,則會在上次wait的地方繼續執行,同樣的給娃做飯,就會出現上圖的效果,爸媽線程交替做飯

解決:將if替換為while,while語句塊每次執行完之后都會重新判斷,知道條件不成立才會結束循環,即可解決

public class KitChenRoom {

    private boolean hasFood = false;

    private Object lock = new Object();

    public void cook() {
        // 加鎖
        synchronized (lock) {
            // 將if替換為while
            while(hasFood) {
                // 還有吃的,先不做飯
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            // 否則就做飯,
            System.out.println(Thread.currentThread().getName() + "沒吃的了,給娃做飯!");
            // 做好之后,修改為true
            hasFood = true;
            // 通知其他線程吃飯
            lock.notifyAll();
        }
    }

    // 吃飯
    public void eat() {
        synchronized (lock) {
            // 將if替換為while
            while (!hasFood) {
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            // 否則就吃飯
            System.out.println(Thread.currentThread().getName() + "感謝老媽,恰飯,恰飯");
            // 吃完之后,修改為false
            hasFood = false;
            // 通知其他線程吃飯
            lock.notifyAll();
        }
    }

}

運行結果:發現做飯和吃飯交替執行

為什么使用while就能解決呢?其實就是 if和while的區別

由于在多線程內容中,有很多小伙伴犯迷,為什么用while就解決了,其實是思路沒有打開,把以前學的東西都忘記了,滿腦子都是多線程的東西,你說是不是!學習要融會貫通,將前后所有的知識點串起來

解決虛假喚醒非常簡單,其實就是利用了while的特性,while體每次執行都會循環再次判斷條件,直到條件不成立跳出循環,在這也是一樣:

  • 媽媽線程執行發現hasFood = true,就進入等待,再次得到cpu時間片執行時,在哪里等待就在哪里醒來繼續執行,也就是再lock.wait()的地方繼續執行
  • 由于該代碼在while循環中,會循環判斷,如果hasFood = true繼續wait,如果hasFood = false就跳出循環,執行循環體之外的代碼
  • 但是如果是if,就只會判斷一次,醒來之后不會再次判斷,因為lock.wait()代碼已經執行過了,會直接向下執行,開始給娃做飯

notify和notifyAll

上邊我們使用notifyAll喚醒了所有線程,如果將notifyAll替換為notify會發生什么?

public class KitChenRoom {

    private boolean hasFood = false;
    private Object lock = new Object();

    public void cook() {
        synchronized (lock) {
            while (hasFood) {
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println(Thread.currentThread().getName() + "沒吃的了,給娃做飯!");
            hasFood = true;
            // // 替換為notify
            lock.notify();
        }
    }

    public void eat() {
        synchronized (lock) {
            while (!hasFood) {
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            hasFood = false;
            // 替換為notify
            lock.notify();
        }
    }
}

運行結果:運行三次,發現前兩次程序卡住不動,產生死鎖,第三次正常執行完

在解釋這個原因之前先搞清楚 鎖池 和 等待池 兩個概念:

  • 鎖池:假設線程A已經擁有了某個對象的鎖【注意:不是類】,而其它的線程想要調用這個對象的某個synchronized方法【或者synchronized塊】,由于這些線程在進入對象的synchronized方法之前必須先獲得該對象的鎖的擁有權,但是該對象的鎖目前正被線程A擁有,所以這些線程就進入了該對象的鎖池中。
  • 等待池:假設一個線程A調用了某個對象的wait()方法,線程A就會釋放該對象的鎖,之后進入到了該對象的等待池中

對象鎖:任何一個對象都可以被當做鎖,所以稱為對象鎖,比如下方代碼lock1和lock2就是兩把對象鎖,都有自己獨立的鎖池和等待池

  • 調用 lock1.wait() 就是該線程進入到lock1對象鎖的等待池中
  • lock1.notify()就是喚醒lock1對象鎖的等待池中的隨機一個等待線程,lock1.notifyAll(); 就是喚醒該等待池中所有等待線程
  • lock1的鎖池和等待池與lock2是獨立的,互不影響,并不會喚醒彼此等待池中的線程
// 鎖1
private Object lock1 = new Object();
// 鎖2
private Object lock2 = new Object();

public void cook() {
    // 使用lock1對象鎖
    synchronized (lock1) {
        lock1.wait();
    }
    lock1.notify();
}

調用wait、notify、notifyAll之后線程變化:

  • 如果線程調用了對象的wait()方法,那么線程便會處于該對象的等待池中,等待池中的線程不會去競爭該對象的鎖。
  • 當有線程調用了對象的notifyAll()方法【喚醒所有該對象等待池中的 wait 線程】或 notify()方法【只隨機喚醒一個該對象等待池中的 wait 線程】,被喚醒的的線程便會進入該對象的鎖池中,鎖池中的線程會去競爭該對象鎖。也就是說,調用了notify后只要一個線程會由等待池進入鎖池,而notifyAll會將該對象等待池內的所有線程移動到鎖池中,等待鎖競爭
  • 優先級高的線程競爭到對象鎖的概率大,假若某線程沒有競爭到該對象鎖,它還會留在鎖池中,唯有線程再次調用wait()方法,它才會重新回到等待池中。而競爭到對象鎖的線程則繼續往下執行,直到執行完了 synchronized 代碼塊,它會釋放掉該對象鎖,這時鎖池中的線程會繼續競爭該對象鎖。

為什么會死鎖呢?

KitChenRoom中有 cook 和 eat 兩個方法都是有同步代碼塊,并且進入while之后就會調用lock對象鎖的wait方法,所以多個調用過cook和eat方法的線程就會進入等待池處于阻塞狀態,等待一個正在運行的線程來喚醒它們。下面分別分析一下使用notify和notifyAll方法喚醒線程的不同之處:

  • 使用notify:notify方法只能喚醒一個線程,其它等待的線程仍然處于wait狀態,假設調用cook方法的線程執行完后,所有的線程都處于等待狀態,此時又執行了notify方法,這時如果喚醒的仍然是一個調用cook方法的線程【比如爸爸線程 將 媽媽線程喚醒】,那么while循環等于true,則此喚醒的線程【媽媽線程】就會調用wait方法,也會處于等待狀態,而且沒有喚醒其他線程,那就芭比Q了,此時所有的線程都處于等待狀態,就發生了死鎖。
  • 使用notifyAll:可以喚醒所有正在等待該鎖的線程,那么所有的線程都會處于運行前的準備狀態(就是cook方法執行完后,喚醒了所有等待該鎖的線程),那么此時,即使再次喚醒一個調用cook方法的線程,while循環等于true,喚醒的線程再次處于等待狀態,那么還會有其它的線程可以獲得鎖,進入運行狀態。

解決wait死鎖的兩種方案:

  • 通過調用notifyAll喚醒所有等待線程
  • 調用 wait(long timeout) 重載方法,設置等待超時時長,在指定時間內還沒被喚醒則自動醒來

下邊仍然是調用 notify 喚醒等待池中的一個線程,但是調用wait(long timeout) 超時等待方法,讓線程進入等待狀態

public class KitChenRoom {

    private boolean hasFood = false;
    private Object lock = new Object();

    public void cook() {
        synchronized (lock) {
            while (hasFood) {
                try {
                    // 超時等待 2 秒
                    lock.wait(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println(Thread.currentThread().getName() + "沒吃的了,給娃做飯!");
            hasFood = true;
            lock.notify();
        }
    }

    public void eat() {
        synchronized (lock) {
            while (!hasFood) {
                try {
                    // 超時等待 2 秒
                    lock.wait(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println(Thread.currentThread().getName() + "感謝老媽,恰飯,恰飯");
            hasFood = false;
            lock.notify();
        }
    }
}

運行結果:運行三次發現,第一次程序陷入了兩次等待2秒之后程序繼續執行,這就是超時自動喚醒,避免了死鎖

總結:

  • notify方法很容易引起死鎖,除非你根據自己的程序設計,確定不會發生死鎖,notifyAll方法則是線程的安全喚醒方法
  • 如果程序允許超時喚醒,則可以使用wait(long timeout)方法
  • wait(long timeout,int nanou):與 wait(long timeout)相同,不過提供了納秒級別的更精確的超時控制

ReentrantLock結合Condition

Condition是JDK1.5新增的接口,在java.util.concurrent.locks 包中,提供了類似的Object的監視器方法,與Lock配合可以實現等待/通知模式,方法作用在下方源碼中已簡單注釋,想要查看詳細說明,強烈建議看源碼,通過翻譯軟件翻譯一下就行!

package java.util.concurrent.locks;

import java.util.concurrent.TimeUnit;
import java.util.Date;

public interface Condition {

    //使當前線程在接到信號或被中斷之前一直處于等待狀態
    void await() throws InterruptedException;

    // 使當前線程在接到信號之前一直處于等待狀態。【注意:該方法對中斷不敏感】。
    void awaitUninterruptibly();

    // 使當前線程在接到信號、被中斷或到達指定等待時間之前一直處于等待狀態。
    // 返回值表示剩余時間,如果在nanosTimesout之前喚醒,那么返回值 = nanosTimeout - 消耗時間,如果返回值 <= 0 ,則可以認定它已經超時了
    long awaitNanos(long nanosTimeout) throws InterruptedException;

    // 使當前線程在接到信號、被中斷或到達指定等待時間之前一直處于等待狀態
    boolean await(long time, TimeUnit unit) throws InterruptedException;

    // 使當前線程在接到信號、被中斷或到達指定最后期限之前一直處于等待狀態。如果沒有到指定時間就被通知,則返回true,否則表示到了指定時間,返回返回false
    boolean awaitUntil(Date deadline) throws InterruptedException;

    // 喚醒一個等待線程。該線程從等待方法返回前必須獲得與Condition相關的鎖。
    void signal();

    // 喚醒所有等待線程。能夠從等待方法返回的線程必須獲得與Condition相關的鎖
    void signalAll();
}

在此我們通過經典的生產者消費者案例說一下Condition實現線程通信,多幾種案例思維更寬闊,多樣化理解對技術刺激更大

案例:有一個快遞點,可以接貨和送貨,最多存放5個包裹,再放就提示包裹已滿,派件時包裹送完就不能再送,提示沒有包裹,不能派送

快遞點:

package com.stt.thread.communication;

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 快遞點:
 * goodsNumber: 快遞數量,默認為0,最多5個,保障原子性使用 AtomicInteger 
 * receiving() : 收貨方法,累加貨物數量,每次 + 1
 * dispatch() : 派送方法,遞減數量,每次 - 1
 * 注意:因為使用 Condition 實現,Condition 需要通過 ReentrantLock 獲取,
 *      所以可以使用 ReentrantLock實現同步就不需要 synchronized
 */
public class ExpressPoint {

    // 快遞數量,使用原子類
    private AtomicInteger goodsNumber = new AtomicInteger();

    // 鎖對象
    private ReentrantLock lock = new ReentrantLock();

    // 創建線程通信對象
    private Condition condition = lock.newCondition();

    // 收貨方法,使用Lock鎖,就不需要synchronized同步了
    public void receiving() {
        // 上鎖
        lock.lock();
        // 寫try...finally,保障無論是否發生異常都可以解鎖,避免死鎖
        try {
            // 如果達到5個,就提示,并且等待
            while (goodsNumber.get() == 5) {
                System.out.println("庫房已滿,已不能再接收!");
                // 等待,有異常拋出
                condition.await();
            }
            System.out.println(Thread.currentThread().getName() + "已收到編號:" + goodsNumber.incrementAndGet() + "的包裹");
            // 喚醒其他線程
            condition.signalAll();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            // 解鎖
            lock.unlock();
        }
    }
    // 派送方法
    public void dispatch() {
        // 上鎖
        lock.lock();
        try {
            // 等于0就不能再派送
            while (goodsNumber.get() == 0) {
                System.out.println("沒有包裹,不能派送!");
                condition.await();
            }
            System.out.println(Thread.currentThread().getName() + "已送出編號:" + goodsNumber.get() + "的包裹");
            goodsNumber.decrementAndGet();
            condition.signalAll();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            // 解鎖
            lock.unlock();
        }
    }
}

測試類:通過while死循環,不斷接貨和送貨

public class ExpressPointMain {

    public static void main(String[] args) {
        ExpressPoint expressPoint = new ExpressPoint();

        // 收貨線程
        new Thread(() -> {
            while (true){
                expressPoint.receiving();
            }
        },"收貨員").start();

        // 送貨線程
        new Thread(() -> {
            while (true){
                expressPoint.dispatch();
            }
        },"送貨員").start();
    }
}

運行結果:發現收貨員線程和送貨員線程交替執行,并且庫存滿和送完之后都有對應的提示

總結:在Condition中,用await()替換wait(),用signal()替換 notify(),用signalAll()替換notifyAll(),對于我們以前使用傳統的Object方法,Condition都能夠給予實現

Condition 精準喚醒

不同的 Condition 可以用來等待和喚醒不同的線程,類似于上邊我們說的等待池,但是Condition是通過隊列實現等待和喚醒,Condition的await()方法,會使得當前線程進入等待隊列并釋放鎖,同時線程狀態變為等待狀態。當從await()返回時,當前線程一定是獲取了Condition相關聯的鎖。Condition實現方式在后邊我們再分析

上邊調用await 和 signalAll方法是控制所有該Condition對象的線程,我們有兩個線程分別為收貨和送貨,我們可以創建兩個Condition對象來精準控制等待和喚醒收貨和送貨線程。

package com.stt.thread.communication;

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
定義兩個 Condition 對象,一個控制收貨線程等待和喚醒,一個控制送貨線程的等待和喚醒
 */
public class ExpressPoint {

    // 快遞數量,使用原子類
    private AtomicInteger goodsNumber = new AtomicInteger();

    // 鎖對象
    private ReentrantLock lock = new ReentrantLock();

    // 創建線程通信對象
    private Condition receivingCondition = lock.newCondition();
    private Condition dispatchCondition = lock.newCondition();

    // 收貨方法,使用Lock鎖,就不需要synchronized同步了
    public void receiving() {
        // 上鎖
        lock.lock();
        // 寫try...finally,保障無論是否發生異常都可以解鎖,避免死鎖
        try {
            // 判斷是否繼續接貨
            while (goodsNumber.get() == 5) {
                System.out.println("庫房已滿,已不能再接收!");
                // 讓收貨線程進入等待
                receivingCondition.await();
            }
            System.out.println(Thread.currentThread().getName() + "已收到編號:" + goodsNumber.incrementAndGet() + "的包裹");
            // 僅僅喚醒送貨線程
            dispatchCondition.signal();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            // 解鎖
            lock.unlock();
        }
    }
    // 派送方法
    public void dispatch() {
        // 上鎖
        lock.lock();
        try {
            // 判斷是否繼續送貨
            while (goodsNumber.get() == 0) {
                System.out.println("沒有包裹,不能派送!");
                // 送貨線程等待
                dispatchCondition.await();
            }
            System.out.println(Thread.currentThread().getName() + "已送出編號:" + goodsNumber.get() + "的包裹");
            goodsNumber.decrementAndGet();
            // 喚醒收貨線程
            receivingCondition.signal();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            // 解鎖
            lock.unlock();
        }
    }
}

運行結果:運行結果是一樣的,只是僅僅會讓對應的線程等待和喚醒

Condition實現分析

等待隊列

Conditiont的等待隊列是一個FIFO隊列,隊列的每個節點都是等待在Condition對象上的線程的引用,該線程就是在Condition對象上等待的線程,如果一個線程調用了Condition.await(),那么該線程就會釋放鎖,構成節點加入等待隊列并進入等待狀態。

從下圖可以看出來Condition擁有首尾節點的引用,而新增節點只需要將原有的尾節點nextWaiter指向它,并更新尾節點即可。上述節點引用更新過程沒有使用CAS機制,因為在調用await()的線程必定是獲取了鎖的線程,該過程由鎖保證線程的安全。

一個Lock(同步器)擁有一個同步隊列和多個等待隊列:

如上邊的例子:就是擁有receivingCondition 和 dispatchCondition兩個等待隊列

private Condition receivingCondition = lock.newCondition();
private Condition dispatchCondition = lock.newCondition();

等待

調用Condition的await()方法,會使得當前線程進入等待隊列并釋放鎖,同時線程狀態變為等待狀態。當從await()返回時,當前線程一定是獲取了Condition相關聯的鎖。

線程觸發await()這個過程可以看作是同步隊列的首節點【當前線程肯定是成功獲得了鎖,才會執行await方法,因此一定是在同步隊列的首節點】移動到了Condition的等待隊列的尾節點,并釋放同步狀態進入等待狀態,同時會喚醒同步隊列的后繼節點

喚醒

  • 調用signal():會喚醒再等待隊列中的首節點,該節點也是到目前為止等待時間最長的節點
  • 調用signalAll():將等待隊列中的所有節點全部喚醒,相當于將等待隊列中的每一個節點都執行一次signal()

CountDownLatch

Java5之后在 java.util.concurrent 也就是【JUC】包中提供了很多并發編程的工具類,如 CountDownLatch 計數器是基于 AQS 框架實現的多個線程之間維護共享變量的類

使用場景

可以通過 CountDownLatch 使當前線程阻塞,等待其他線程完成給定任務,比如,等待線程完成下載任務之后,提示用戶下載完成;導游等待所有游客參觀完之后去下一個景點等

使用介紹

CountDownLatch的構造函數接收一個int類型的參數作為計數器,如果你想等待n個點完成,這里就傳入n。這里所說的n個點,可以是n個線程,也可以是1個線程里的n個執行步驟。CountDownLatch 構造函數如下:

public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
 }

計數器參數count必須大于等于0,等于0的時候,調用await方法時不會阻塞當前線程。

當我們調用CountDownLatch的countDown()方法時,n就會減1,CountDownLatch的await()方法會阻塞當前線程,直到n變成零,繼續執行。

CountDownLatch 方法

  • await():阻塞當前線程,直到計數器為零為止
  • await(long timeout, TimeUnit unit):await()的重載方法,可以指定阻塞時長
  • countDown():計數器減1,如果計數達到零,釋放所有等待的線程
  • getCount(): 返回當前計數

案例:比如開一把英雄聯盟,需要10個人加載完成才會進入游戲,可以理解為10個線程運行完畢之后進入游戲頁面

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CountDownLatch;

public class LoadingGame {

    public static void main(String[] args) {
        // 計數器
        CountDownLatch latch = new CountDownLatch(10);
        // 玩家數組
        String[] player = new String[10];
        // 隨機數,用來加載進度條時線程睡眠使用,防止直接加載到100
        Random random = new Random();
        // 循環開啟10個線程,即10個玩家
        for (int i = 0; i < 10; i++) {
            // 記錄玩家在數組中的下標
            int index = i;
            new Thread(() -> {
                // 循環進度條到100
                for (int j = 0; j <= 100; j++) {
                    try {
                        // 每加載 1% 就隨機睡眠一段時間
                        Thread.sleep(random.nextInt(100));
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    // 修改指定玩家進度條
                    player[index] = j +"%";
                    // 輸出當前所有的玩家進度
                    System.out.print("r" + Arrays.toString(player));
                }
                // 每加載完一個玩家計數-1
                latch.countDown();
            }).start();
        }
        try {
            // 阻塞當前線程【main線程】,等待十個玩家加載結束后喚醒
            latch.await();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("n"+"游戲開始");
    }
}

運行結果:發現主線程等待10個子線程加載到100時才執行

高頻面試題——如何保證多個線程按順序執行

其實就是讓線程按照指定的順序一個一個執行,這里結合同一案例給大家介紹4種方法:

案例:老師布置作業之后,學生開始寫作業,學生寫完作業老師批改,之后老師再將學生的作業情況記錄下來,這個順序不可錯亂

Thread的join方法

public class HomeworkJoin {
    public static void main(String[] args) {

        // 布置作業線程
        Thread t1 = new Thread(() -> {
            System.out.println("......老師布置作業......");
        });

        // 學生寫作業,需要等待老師布置完
        Thread t2 = new Thread(() -> {
            try {
                // t1插入執行,也就是插隊
                t1.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("......學生寫作業......");
        });


        // 學生寫作業,需要等待老師布置完
        Thread t3 = new Thread(() -> {
            try {
                // t2插隊
                t2.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("......老師檢查作業......");
        });

        // 學生寫作業,需要等待老師布置完
        Thread t4 = new Thread(() -> {
            try {
                // t3插隊
                t3.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("......老師記錄作業情況......");
        });

        // 開啟線程
        t1.start();
        t2.start();
        t3.start();
        t4.start();

        // t1線程插隊
        try {
            t4.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("......作業布置和檢查結束......");
    }
}

運行結果:

使用Condition(條件變量)

我們可以使用Condition精確喚醒下一個需要執行的線程

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class HomeworkCondition {
    // 鎖對象
    private static Lock lock = new ReentrantLock();
    // 阻塞隊列
    private static Condition doWork = lock.newCondition();
    private static Condition checkWork = lock.newCondition();
    private static Condition recordWork = lock.newCondition();

    /**
     * 為什么要加這三個標識狀態?
     * 如果沒有狀態標識,線程就無法正確喚醒,就一直處于等待狀態
     */
    private static Boolean t1Run = false;
    private static Boolean t2Run = false;
    private static Boolean t3Run = false;

    public static void main(String[] args) {
        // 布置作業線程
        Thread t1 = new Thread(() -> {
            lock.lock();
            try {
                System.out.println("......老師布置作業......");
                // t1執行完畢
                t1Run = true;
                // 喚醒doWork等待隊列中的第一個線程
                doWork.signal();
            }finally {
                lock.unlock();
            }

        });
        // 學生寫作業,需要等待老師布置完
        Thread t2 = new Thread(() -> {
            lock.lock();
            try {
                // 判斷是否布置作業
                if(!t1Run) {
                    // 還沒布置作業,先不寫作業,進入等待隊列
                    doWork.await();
                }
                System.out.println("......學生寫作業......");
                t2Run = true;
                // 喚醒checkWork等待隊列第一個線程
                checkWork.signal();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
            }

        });

        // 老師家查作業,需要學生寫完
        Thread t3 = new Thread(() -> {
            lock.lock();
            try {
                // 判斷學生是否寫完作業
                if(!t2Run) {
                    // 沒寫完,先不檢查,進入等待隊列
                    checkWork.await();
                }
                System.out.println("......老師檢查作業......");
                t3Run = true;
                // 喚醒recordWork等待隊列第一個線程
                recordWork.signal();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
            }

        });

        // 老師上傳作業情況,需要檢查完
        Thread t4 = new Thread(() -> {
            lock.lock();
            try {
                if(!t3Run) {
                    recordWork.await();
                }
                System.out.println("......老師記錄作業情況......");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
            }
        });

        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

使用CountDownLatch(倒計數)

聲明三個 CountDownLatch 計數器,初始只都為 1,每次執行上一部操作之后下一步操作的計數器 -1,當計數器值為0時就繼續執行,否則就陷入等待

import java.util.concurrent.CountDownLatch;

public class HomeworkCountDownLatch {

    public static void main(String[] args) {
        // 創建三個計數器
        CountDownLatch doWork = new CountDownLatch(1);
        CountDownLatch checkWork = new CountDownLatch(1);
        CountDownLatch recordWork = new CountDownLatch(1);

        // 布置作業線程
        Thread t1 = new Thread(() -> {
            System.out.println("......老師布置作業......");
            // 布置作業之后,做作業計數器 -1
            doWork.countDown();
        });

        // 學生寫作業,需要等待老師布置完
        Thread t2 = new Thread(() -> {
            try {
                doWork.await();
                System.out.println("......學生寫作業......");
                // 對 檢查作業 -1
                checkWork.countDown();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });


        // 學生寫作業,需要等待老師布置完
        Thread t3 = new Thread(() -> {
            try {
                doWork.await();
                System.out.println("......老師檢查作業......");
                // 對 錄入作業情況 -1
                recordWork.countDown();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

        });

        // 學生寫作業,需要等待老師布置完
        Thread t4 = new Thread(() -> {
            try {
                recordWork.await();
                System.out.println("......老師記錄作業情況......");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

使用CyclicBarrier(回環柵欄)

CyclicBarrier可以實現讓一組線程等待至某個狀態之后再全部同時執行,【回環】是因為當所有等待線程都被釋放以后,CyclicBarrier可以被重用,可以把這個狀態當做barrier,當調用await()方法之后,線程就處于barrier了。示例如下:

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class HomeworkCyclicBarrier {
    public static void main(String[] args) {

        CyclicBarrier doWork = new CyclicBarrier(2);
        CyclicBarrier checkWork = new CyclicBarrier(2);
        CyclicBarrier recordWork = new CyclicBarrier(2);

        // 布置作業線程
        Thread t1 = new Thread(() -> {
            try {
                System.out.println("......老師布置作業......");
                //放開柵欄1
                doWork.await();
            } catch (BrokenBarrierException e) {
                throw new RuntimeException(e);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });

        // 學生寫作業,需要等待老師布置完
        Thread t2 = new Thread(() -> {
            try {
                //放開柵欄1
                doWork.await();
                System.out.println("......學生寫作業......");
                //放開柵欄2
                checkWork.await();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } catch (BrokenBarrierException e) {
                throw new RuntimeException(e);
            }
        });


        // 學生寫作業,需要等待老師布置完
        Thread t3 = new Thread(() -> {

            try {
                //放開柵欄2
                checkWork.await();
                System.out.println("......老師檢查作業......");
                //放開柵欄3
                recordWork.await();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } catch (BrokenBarrierException e) {
                throw new RuntimeException(e);
            }

        });

        // 學生寫作業,需要等待老師布置完
        Thread t4 = new Thread(() -> {
            try {
                //放開柵欄3
                recordWork.await();
                System.out.println("......老師記錄作業情況......");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } catch (BrokenBarrierException e) {
                throw new RuntimeException(e);
            }

        });
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

此四種方法都可以實現同樣的效果,當然你也可以使用Object的wait() 和 notify()/notifyAll()實現

高頻面試題——Thread.join()和CountDownLatch的區別

  • Thread.join()是Thread類的一個方法,Thread.join()的實現是依靠Object的wait()和notifyAll()來完成的,而CountDownLatch是JUC包中的一個工具類

當我們使用ExecutorService 【線程池】,就不能使用join,必須使用CountDownLatch,比如:

ExecutorService service = Executors.newFixedThreadPool(5);
final CountDownLatch latch = new CountDownLatch(5);
for(int x = 0; x < 5; x++) {
    service.submit(new Runnable() {
        public void run() {
            // do something
            latch.countDown();
        }
    });
}
latch.await();
  • 調用join方法需要等待thread執行完畢才能繼續向下執行,而CountDownLatch只需要檢查計數器的值為零就可以繼續向下執行,相比之下,CountDownLatch更加靈活一些,可以實現一些更加復雜的業務場景。

為什么wait, notify和notifyAll這些方法在Object類中不在Thread類里面?

Java提供的鎖是對象級的而不是線程級的,線程為了進入臨界區【也就是同步塊內】,需要獲得鎖并等待鎖可用,它們并不知道也不需要知道哪些線程持有鎖,它們只需要知道當前資源是否被占用,是否可以獲得鎖,所以鎖的持有狀態應該由同步監視器來獲取,而不是線程本身。

如果Java不提供關鍵字來解決線程之間的通信,鎖是對象級別,由于wait,notify,notifyAll都是鎖級別的操作,每個對象都可以當做鎖所以把他們定義在Object類中是最合適的。?

分享到:
標簽:Java
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定