執行緒間協作

eacape發表於2022-04-22

等待與通知

  1. 在java平臺可以通過使用Object.wait()/Object.wait(long)和Object.notify()/Object.notifyAll()配合來實現執行緒的等待與通知。

    Object.wait()能夠使當前執行緒暫停(狀態轉為WAITING),該方法可以用來實現等待,其所在的執行緒稱為等待執行緒。

    Object.notify()可以喚醒一個等待執行緒,其所線上程被稱為通知執行緒。

    wait()和notify()方法都使Object類的方法,因為其是所有類的父類,所以,所有類都有這兩個方法。

    synchronized(lockObject){
      while(等待條件){
        lockObject.wait();
      }
      ......
      //後續操作
    }

    上面程式碼中,while的判斷條件,我們暫且稱為等待條件,當這個條件成立,這個執行緒就會進入等待條件,當其它執行緒重新將其喚醒,然後再次判斷等待條件成不成立,若不成立表示執行緒可以繼續往下執行做相應的操作,否則繼續進入等待狀態。

    下面我們來思考一下,while的作用,等待條件為什麼要和while配合使用,而不是和if配合使用。

    /**
     * @ClassName WaitIfSample
     * @description:
     * @author: yong.yuan
     * @create: 2022-04-15 16:44
     * @Version 1.0
     **/
    public class WaitIfExample {
        static AtomicInteger stock = new AtomicInteger();
        static final Object LOCKER = new Object();
    
        static class Consumer implements Runnable{
            @Override
            public void run() {
                consume();
            }
    
            void consume(){
                synchronized (LOCKER){
                    while (stock.get() == 0) {
                        try {
                            LOCKER.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    stock.getAndDecrement();
                    System.out.println("consumer " + Thread.currentThread().getName()
                     + "消費訊息後庫存為:"+stock.get());
                    LOCKER.notifyAll();
                }
            }
        }
    
        static class Producer implements Runnable{
            @Override
            public void run() {
                product();
            }
            void product(){
                synchronized (LOCKER) {
                    while (stock.get() != 0) {
                        try {
                            LOCKER.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    stock.getAndAdd(2);
                    System.out.println("producer 生產訊息,生產後庫存為:"+stock.get());
                    LOCKER.notifyAll();
                }
            }
        }
    
        public static void main(String[] args) {
            Consumer consumer = new Consumer();
            new Thread(consumer).start();
            new Thread(consumer).start();
            new Thread(consumer).start();
    
            new Thread(new Producer()).start();
        }
    }

    上面我們實現一個簡單的生產者-消費者的這樣一個訊息佇列,對於消費者而言只有在庫存大於0的時候才能進行消費,假設現在有3個消費執行緒正在執行,但是初始化的時候庫存為0,所以這3個執行緒就都進入到了WAITING狀態,然後有一個生產者在庫存中增加了2,然後對所有等待執行緒進行喚醒,喚醒的執行緒會先通過while判斷等待條件到底成不成立,成立→繼續等待,不成立→向後執行消費動作,程式碼中的3個等待執行緒中有2個將會消費成功,有1個會繼續進入等待狀態。

    producer 生產訊息,生產後庫存為:2
    consumer Thread-2消費訊息後庫存為:1
    consumer Thread-1消費訊息後庫存為:0

    但是,如果將while換成if呢?當等待執行緒被喚醒後,即使,等待條件成立,執行緒也會繼續執行,顯然這不是我們想要的結果。

    producer 生產訊息,生產後庫存為:2
    consumer Thread-2消費訊息後庫存為:1
    consumer Thread-1消費訊息後庫存為:0
    consumer Thread-0消費訊息後庫存為:-1

    當然while配合等待條件只是一種通用場景,有的特殊場景不使用while或者使用if也是可以的。

  2. Object.wait(long)允許我們指定一個超時時間(單位為毫秒),如果等待執行緒在這個時間內沒有被其它執行緒喚醒,那麼java虛擬機器會自動喚醒這個執行緒,不過這樣既不會丟擲異常也沒有返回值,所以執行緒是否自動喚醒需要一些額外操作。
  3. wait/notify的開銷及問題

    • 過早喚醒:A、B、C三個執行緒中都使用同一個鎖物件,而且都存線上程暫停的判斷,但是A和B使用的執行緒暫停的判斷條件和C是不同的,所以當A、B、C同時處於WAITING狀態時,有一個執行緒為了喚醒A、B會使用notifyAll(),這樣同時也會把C喚醒,但是C的通過while判斷還是繼續進入到了暫停狀態,也就是說這個notify動作是與C沒有太大關聯的,這就被稱作過早喚醒。
    • 訊號丟失:如果等待執行緒在執行Object.wait()前沒有先判斷保護條件是否已然成立,那麼有可能出現這種情形——通知執行緒在該等待執行緒進入臨界區之前就已經更新了相關共享變數,使得相應的保護條件成立並進行了通知,但是此時等待執行緒還沒有被暫停,自然也就無所謂喚醒了。這就可能造成等待執行緒直接執行Object.wait()而被暫停的時候,該執行緒由於沒有其他執行緒進行通知而一直處於等待狀態。這種現象就相當於等待執行緒錯過了一個本來“傳送”給它的“訊號”,因此被稱為訊號丟失。
    • 欺騙性喚醒:執行緒可能在沒有其他執行緒執行notify/notifyAll的情況下被喚醒,這就被稱為欺騙性喚醒。在wait()方法外面加while()進行判斷就可以解決這個問題。
    • 上下文切換:wait/notify對應著是執行緒暫停/執行緒喚醒,所以會導致多次鎖的申請與釋放,鎖的申請與釋放可能會造成上下文切換。
  4. java虛擬機器為每個物件維護一個被稱為 等待集(wait set)的佇列,該佇列用於儲存該物件上的等待執行緒,Object.wait()會讓當前執行緒暫停並釋放相應的鎖,並將當前執行緒存入物件的等待集中。執行Object.notify()會使該物件的等待集中的任意一個執行緒被喚醒,喚醒的執行緒並不會立即被從這個等待集中移除,而是,等到這個執行緒在次持有物件鎖的時候才會被移除。
  5. Thread.join():某個執行緒執行完了,此執行緒才能執行

    static void main(){
      Thread t = new Thread();
      t.start();
      ......
      t.join();//A
      ......
    }

    以上為例,只有t執行緒執行完畢後,主執行緒才能執行A後面的內容。

條件變數

  1. Condition可以作為wait/notify的替代品來實現等待/通知,它的await()、signal()/signalAll()分別對應wait()、notify()/notifyAll()並且解決了過早喚醒以及wait(long)是否超時無法判斷等問題。
  2. Object.wait()/Object.notify()要求執行執行緒持有該物件的內部鎖。

    Condition.await()/Condition.signal()要求執行執行緒持有該物件的顯式鎖。

  3. Condition.awaitUntil(Date),引數是等待的截至期限,當awaitUntil被其它執行緒signal時這個方法會返回true。
  4. Condition使用樣例

    /**
     * @ClassName ConditionSimple
     * @description:
     * @author: yong.yuan
     * @create: 2022-04-18 10:40
     * @Version 1.0
     **/
    public class ConditionExample {
        static Lock lock = new ReentrantLock();
        static Condition conditionA = lock.newCondition();
        static Condition conditionB = lock.newCondition();
        static BlockingQueue<String> queue = new ArrayBlockingQueue<String>(3);
    
        static class Consumer implements Runnable{
            @Override
            public void run() {
                lock.lock();
                try {
                    while (queue.isEmpty()){
                        System.out.println("消費者暫停中....");
                        conditionA.await();
                    }
                    System.out.println("消費執行緒"+Thread.currentThread().getName()
                    +"消費訊息:"+queue.take());
                    conditionB.signalAll();
                }catch (InterruptedException e){
                    e.printStackTrace();
                }finally {
                    lock.unlock();
                }
            }
        }
    
        static class Producer implements Runnable{
            @Override
            public void run() {
                lock.lock();
                try {
                    while (queue.remainingCapacity() == 0){
                        System.out.println("生產者暫停中...");
                        conditionB.await();
                    }
                    System.out.println("生產執行緒"+Thread.currentThread().getName()
                    +"生產訊息...");
                    queue.add("hello");
                    conditionA.signalAll();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        }
    }
    

倒數計時協調器

CountDownLatch是一般用來實現一個其他執行緒等待其他執行緒完成一組特定的操作後再繼續執行,這組特定的操作被稱為先決操作。

CountDownLatch會有一個維護沒有完成的先決操作的數量的計數器countDownLatch.countDown() 每被執行一次,計數器就會-1,CountDownLatch.await()相當於一個受保護方法,當它所線上程執行到這個方法後,執行緒就會暫停,直到它內部鎖維護的計數器為0時,執行緒才會被喚醒,繼續向下執行。

當CountDownLatch中的計數器為0時,再次執行countDown方法,計數器的值不會變,也不會丟擲異常,再次執行await方法執行緒也不會停止,這意味著CountDownLatch的使用是一次性的。

使用CountDownLatch實現等待/通知的時候呼叫await、countDown方法都無須加鎖。

使用場景:例如啟動java服務,各服務之間有呼叫關係,一般先啟動相應數量的被呼叫服務。例如A服務啟動5個例項後才能啟動B服務,那就可以有一個初始值為5的CountDownLatch,在B服務啟動前設定await,每一個A服務啟動就countDown,當A服務例項啟動完畢後,B才開始啟動。

public class CountDownLatchExample {
  private static final CountDownLatch latch = new CountDownLatch(4);
  private static int data;

  public static void main(String[] args) throws InterruptedException {
    Thread workerThread = new Thread() {
      @Override
      public void run() {
        for (int i = 1; i < 10; i++) {
          data = i;
          latch.countDown();
          // 使當前執行緒暫停(隨機)一段時間
          Tools.randomPause(1000);
        }

      };
    };
    workerThread.start();
    latch.await();
    Debug.info("It's done. data=%d", data);
  }
}

柵欄

CyclicBarrier和CountDownLatch有一定相似之處,CountDownLatch是一個await的執行緒要等待其它執行緒完成一定數量的先決條件才能繼續執行,CyclicBarrier會在多個執行緒中設定一個await點,到達這個點的執行緒數量達到了設定的數量要求才會繼續執行。

/**
 * @ClassName ClimbMountains
 * @description:
 * @author: yong.yuan
 * @create: 2022-04-03 21:59
 * @Version 1.0
 **/
public class ClimbMountains {
    static Logger logger = Logger.getLogger(ClimbMountains.class.getName());
    static CyclicBarrier[] climbBarrier = new CyclicBarrier[2];

    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            Company company = new Company(i);
            company.start();
        }
    }
    static class Company{
        private final String name;
        private final List<Staff> staffList;
        public Company(int c) {
            this.name ="公司".concat(String.valueOf(c));
            climbBarrier[c] = new CyclicBarrier(5);
            staffList = new ArrayList<>();
            for (int j = 0; j < 5; j++) {
                staffList.add(new Staff(name,c,j));
            }
        }

        synchronized void start() {
            String log = String.format("%s 五位精英開始攀登....",name);
            logger.info(log);
            for (Staff staff:staffList) {
                new Thread(staff::start).start();
            }
        }
    }

    static class Staff{
        private final String name;
        private final int c;
        public Staff(String company,int c,int s) {
            this.c = c;
            this.name = company.concat("-員工").concat(String.valueOf(s));
        }

        void start() {
            System.out.println(name + ":開始攀登!");
            try {
                int time = new Random().nextInt(20000);
                Thread.sleep(time);
                System.out.println(name+":用時"+time/1000+"秒");
                climbBarrier[c].await();
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            }finally {
                System.out.println(name + ":完畢");
            }
        }
    }
}
=================輸出結果=================
四月 18, 2022 4:00:49 下午 io.github.viscent.mtia.ch5.ClimbMountains$Company start
資訊: 公司0 五位精英開始攀登....
公司0-員工0:開始攀登!
公司0-員工1:開始攀登!
公司0-員工2:開始攀登!
公司0-員工3:開始攀登!
公司0-員工4:開始攀登!
公司1-員工0:開始攀登!
公司1-員工1:開始攀登!
公司1-員工2:開始攀登!
公司1-員工3:開始攀登!
公司1-員工4:開始攀登!
四月 18, 2022 4:00:49 下午 io.github.viscent.mtia.ch5.ClimbMountains$Company start
資訊: 公司1 五位精英開始攀登....
公司1-員工3:用時4秒
公司1-員工4:用時4秒
公司0-員工0:用時5秒
公司0-員工3:用時7秒
公司0-員工2:用時9秒
公司1-員工2:用時11秒
公司1-員工1:用時12秒
公司1-員工0:用時13秒
公司1-員工0:完畢
公司1-員工1:完畢
公司1-員工2:完畢
公司1-員工4:完畢
公司1-員工3:完畢
公司0-員工4:用時13秒
公司0-員工1:用時16秒
公司0-員工0:完畢
公司0-員工4:完畢
公司0-員工2:完畢
公司0-員工3:完畢
![](https://eacape-1259159524.cos.ap-shanghai.myqcloud.com/images/clipboard.png)
公司0-員工1:完畢

阻塞佇列

阻塞佇列可以按照其儲存空間是否受限制劃分為有界佇列和無界佇列,有界佇列的佇列容量是由程式設定的,無界佇列的容量是Integer.MAX\_VALUE也就是2^31

常用阻塞佇列和常用方法

  1. ArrayBlockingQueue

    其底層的資料結構是一個陣列,所以它在put和take的時候不會造成垃圾回收的負擔,它在put和take的時候使用的是同一個顯式鎖,所以造成他在put和take的時候會伴隨著鎖的釋放與申請,如果有大量執行緒在不斷的put和take會造成鎖競爭過高,從而不斷導致上下文切換。

  2. LinkedBlockingQueue

    其底層的資料結構是一個連結串列,所以它在put個take的時候會伴隨著空間的動態分配,也就是每此put或take操作都會伴隨著節點的建立和移除,這樣就會造成垃圾回收的負擔。但是,它的put和take操作是使用的兩個不一樣的顯式鎖,這樣相對會減緩鎖競爭度。

    此外其內部維護著一個Atomic變數用於維護佇列長度,也可能存在被put執行緒和take執行緒不斷的爭用。

  3. SychronousQueue

    容量為0,主要時用來轉發任務(阻塞作用),SychronousQueue.take(E)的時候沒有執行緒執行SychronousQueue.put(E)那麼消費執行緒就會暫停知道有生產執行緒執行SychronousQueue.put(E),

    同樣,SychronousQueue.put(E)的時候沒有消費執行緒執行SychronousQueue.take(E),生產執行緒也會停止直到,有消費執行緒執行SychronousQueue.take(E)。

    SychronousQueue和ArrayBlockingQueue/LinkedBlockingQueue前者就像送快遞時快遞員要把快遞交到你的手上才能去送下一份快遞,而後者就是直接把快遞放到蜂巢儲物櫃中。

  4. PriorityBlockingQueue

    支援優先順序的無界阻塞佇列,可以通過自定義的類實現compareTo方法指定元素的 排序規則,它take時如果佇列為空將會阻塞,但是它會無限擴容所以,put並不會 阻塞。其實現原理是堆排序

    在最小堆[1, 5, 8, 6, 10, 11, 20]中再插入一個元素4,下面用圖示分析插入的過程:

    最大堆[20, 10, 15, 6, 9, 10, 12]中移除元素後,下面用圖示分析重排的過程:

  5. DelayWorkQueue

    DelayWorkQueue是實現了延遲功能的PriorityBlockingQueue

    內部採用的時堆結構(插入時會進行排序),特點是內部元素不會按照入隊的順序來出隊,

    而是會根據延時長短對內部元素進行排序。

ArrayBlockingQueue和SynchronousQueue都既支援非公平排程也支援公平排程,而LinkedBlockingQueue僅支援非公平排程。

如果生產者執行緒和消費者執行緒之間的併發程度比較大,那麼這些執行緒對傳輸通道內部所使用的鎖的爭用可能性也隨之增加。這時,有界佇列的實現適合選用LinkedBlockingQueue,否則我們可以考慮ArrayBlockingQueue。

  • LinkedBlockingQueue適合在生產者執行緒和消費者執行緒之間的併發程度比較大的情況下使用
  • ArrayBlockingQueue適合在生產者執行緒和消費者執行緒之間的併發程度較低的情況下使用
  • SynchronousQueue適合在消費者處理能力與生產者處理能力相差不大的情況下使用。

流量控制與訊號量

SemaPhore通常叫做訊號量,一般用來控制同時訪問特定資源的的執行緒數量,通過協調各個執行緒,以保證合理使用資源。

通常使用的場景就是限流,比如說,限制資料庫連線池的連線執行緒數量。

常用方法

acquire()  
獲取一個令牌,在獲取到令牌、或者被其他執行緒呼叫中斷之前執行緒一直處於阻塞狀態。
​
acquire(int permits)  
獲取一個令牌,在獲取到令牌、或者被其他執行緒呼叫中斷、或超時之前執行緒一直處於阻塞狀態。
    
acquireUninterruptibly() 
獲取一個令牌,在獲取到令牌之前執行緒一直處於阻塞狀態(忽略中斷)。
    
tryAcquire()
嘗試獲得令牌,返回獲取令牌成功或失敗,不阻塞執行緒。
​
tryAcquire(long timeout, TimeUnit unit)
嘗試獲得令牌,在超時時間內迴圈嘗試獲取,直到嘗試獲取成功或超時返回,不阻塞執行緒。
​
release()
釋放一個令牌,喚醒一個獲取令牌不成功的阻塞執行緒。
​
hasQueuedThreads()
等待佇列裡是否還存在等待執行緒。
​
getQueueLength()
獲取等待佇列裡阻塞的執行緒數。
​
drainPermits()
清空令牌把可用令牌數置為0,返回清空令牌的數量。
​
availablePermits()
返回可用的令牌數量。

實現一個簡單的令牌獲取的例子

/**
 * @ClassName SemaphoreExample
 * @description:
 * @author: yong.yuan
 * @create: 2022-04-18 18:59
 * @Version 1.0
 **/
public class SemaphoreExample {
    static final Semaphore semaphore = new Semaphore(5);
    static Lock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();
    static CountDownLatch countDownLatch = new CountDownLatch(10);

    static class Worker implements Runnable{
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
                semaphore.acquire();
                System.out.println(Thread.currentThread().getId()+
                "號工人,從流水線上取貨物一件,現有" + semaphore.availablePermits() + "件貨物");
                countDownLatch.countDown();
                lock.lock();
                try {
                    condition.signalAll();
                }finally {
                    lock.unlock();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    static class Machine implements Runnable{
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
                while(semaphore.availablePermits() >= 5) {
                    lock.lock();
                    try {
                        System.out.println("流水線上的貨物滿了");
                        condition.await();
                    } finally {
                        lock.unlock();
                    }
                }
                semaphore.release();
                System.out.println("向流水線上送貨物,現有"+semaphore.availablePermits()
                +"件貨物");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            new Thread(new Worker()).start();
            new Thread(new Machine()).start();
        }

        countDownLatch.await();
        System.out.println("已經有10個工人搬走貨物了");
        while (semaphore.tryAcquire()){
            System.out.println("剩餘貨物搬走.....");
        }
    }
}
==============執行結果==============
流水線上的貨物滿了
流水線上的貨物滿了
17號工人,從流水線上取貨物一件,現有4件貨物
向流水線上送貨物,現有4件貨物
流水線上的貨物滿了
向流水線上送貨物,現有5件貨物
19號工人,從流水線上取貨物一件,現有5件貨物
13號工人,從流水線上取貨物一件,現有4件貨物
向流水線上送貨物,現有5件貨物
流水線上的貨物滿了
向流水線上送貨物,現有4件貨物
27號工人,從流水線上取貨物一件,現有3件貨物
15號工人,從流水線上取貨物一件,現有4件貨物
向流水線上送貨物,現有5件貨物
向流水線上送貨物,現有5件貨物
25號工人,從流水線上取貨物一件,現有4件貨物
21號工人,從流水線上取貨物一件,現有4件貨物
向流水線上送貨物,現有5件貨物
23號工人,從流水線上取貨物一件,現有4件貨物
向流水線上送貨物,現有5件貨物
流水線上的貨物滿了
向流水線上送貨物,現有5件貨物
29號工人,從流水線上取貨物一件,現有4件貨物
向流水線上送貨物,現有5件貨物
31號工人,從流水線上取貨物一件,現有5件貨物
已經有10個工人搬走貨物了
剩餘貨物搬走.....
剩餘貨物搬走.....
剩餘貨物搬走.....
剩餘貨物搬走.....
剩餘貨物搬走.....

Exchager

Exchanger類可用於兩個執行緒之間交換資訊。可簡單地將Exchanger物件理解為一個包含兩個格子的容器,通過exchanger方法可以向兩個格子中填充資訊。當兩個格子中的均被填充時,該物件會自動將兩個格子的資訊交換,然後返回給執行緒,從而實現兩個執行緒的資訊交換。

當消費者執行緒消費一個已填充的緩衝區時,另外一個緩衝區可以由生產者執行緒進行填充,從而實現了資料生成與消費的併發。這種緩衝技術就被稱為雙緩衝

/**
 * @ClassName ExchangerExample
 * @description:
 * @author: yong.yuan
 * @create: 2022-04-18 23:10
 * @Version 1.0
 **/
public class ExchangerExample {
    static Exchanger<String> STRING_EXCHANGER = new Exchanger<>();
    static class MyThread extends Thread{
        String msg;
        public MyThread(String threadName,String msg) {
            Thread.currentThread().setName(threadName);
            this.msg = msg;
        }

        @Override
        public void run() {
            try {
                System.out.println(Thread.currentThread().getName()+":"
                +STRING_EXCHANGER.exchange(msg));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        Thread t1 = new MyThread("thread-1","hello thread-1");
        Thread t2 = new MyThread("thread-2","hello thread-2");
        t1.start();
        t2.start();
    }
}
===============操作結果===============
Thread-1:hello thread-1
Thread-0:hello thread-2

如何正確的停止執行緒

使用interrupt終止執行緒

  1. 使用interrupt實際上是通過interrupt狀態的變化來對執行緒實現停止,而不會立即終止這個執行緒。
  2. 當執行緒處於wait()或者sleep()狀態時,使用interrupt可以將休眠的執行緒喚醒,但是會丟擲異常,我們可以在Catch(InterruptedExcetion e){}中手動用interrupt()來終止這個執行緒
  3. 對比其它終止方式 - stop():會直接把執行緒停掉,不能處理停止之前想要處理的資料
public class StopThread{
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            int count = 0;
            while (!Thread.currentThread().isInterrupted() && count < 1000){
                System.out.println("count = " + count++);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    //執行緒在休眠期間被中斷,那麼會自動清除中斷訊號,所以需要在catch中再次中斷
                    Thread.currentThread().interrupt();
                }
            }
        });
        thread.start();
        Thread.sleep(5000);
        thread.interrupt();
    }
}

相關文章