天天看點

三個線程按順序列印ABC?十二種做法,深入多線程同步通信機制

作者:IT網際網路新資訊

大家好,我是老三,這篇文章分享一道非常不錯的題目:三個線程按序列印ABC。

很多讀者朋友應該都覺得這道題目不難,這次給大家帶來十二種做法,一定有你沒有見過的新姿勢。

1. synchronized+wait+notify

說到同步,我們很容易就想到synchronized。

線程間通信呢?我們先回憶一下線程間的排程。

三個線程按順序列印ABC?十二種做法,深入多線程同步通信機制

多線程常見排程方法

可以看到,等待和運作之間的轉換可以用wait和notify。

那麼整體思路也就有了:

  • 列印的時候需要擷取鎖
  • 列印B的線程需要等待列印A線程執行完,列印C的線程需要等待列印B線程執行完
三個線程按順序列印ABC?十二種做法,深入多線程同步通信機制

ABC-1

  • 代碼
public class ABC1 {
    //鎖住的對象
    private final static Object lock = new Object();
    //A是否已經執行
    private static boolean aExecuted = false;
    //B是否已經執行過
    private static boolean bExecuted = false;

    public static void printA() {
        synchronized (lock) {
            System.out.println("A");
            aExecuted = true;
            //喚醒所有等待線程
            lock.notifyAll();
        }
    }

    public static void printB() throws InterruptedException {
        synchronized (lock) {
            //擷取到鎖,但是要等A執行
            while (!aExecuted) {
                lock.wait();
            }
            System.out.println("B");
            bExecuted = true;
            lock.notifyAll();
        }
    }

    public static void printC() throws InterruptedException {
        synchronized (lock) {
            //擷取到鎖,但是要等B執行
            while (!bExecuted) {
                lock.wait();
            }
            System.out.println("C");
        }
    }

}
           
  • 測試:後面幾種方法的單測基本和這種方法一緻,是以後面的單測就省略了。
@Test
    void abc1() {
        //線程A
        new Thread(() -> {
            ABC1.printA();
        }, "A").start();
        //線程B
        new Thread(() -> {
            try {
                ABC1.printB();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "B").start();
        //線程C
        new Thread(() -> {
            try {
                ABC1.printC();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "C").start();
    }
           

2. lock+全局變量state

還可以用lock+state來實作,大概思路:

  • 用lock來實作同步
  • 用全局變量state辨別改哪個線程執行,不執行就釋放鎖
三個線程按順序列印ABC?十二種做法,深入多線程同步通信機制

lock+state

  • 代碼
public class ABC2 {
    //可重入鎖
    private final static Lock lock = new ReentrantLock();
    //判斷是否執行:1表示應該A執行,2表示應該B執行,3表示應該C執行
    private static int state = 1;

    public static void printA() {
        //自旋
        while (state < 4) {
            try {
                //擷取鎖
                lock.lock();
                //并發情況下,不能用if,要用循環判斷等待條件,避免虛假喚醒
                while (state == 1) {
                    System.out.println("A");
                    state++;
                }
            } finally {
                //要保證不執行的時候,鎖能釋放掉
                lock.unlock();
            }
        }
    }

    public static void printB() throws InterruptedException {
        while (state < 4) {
            try {
                lock.lock();
                //擷取到鎖,應該執行
                while (state == 2) {
                    System.out.println("B");
                    state++;
                }
            } finally {
                lock.unlock();
            }
        }
    }

    public static void printC() throws InterruptedException {
        while (state < 4) {
            try {
                lock.lock();
                while (state == 3) {
                    //擷取到鎖,應該執行
                    System.out.println("C");
                    state++;
                }
            } finally {
                lock.unlock();
            }
        }
    }

}
           

這裡也有幾個細節要注意:

  • 要在循環裡擷取鎖,不然線程可能會在擷取到鎖之前就終止了
  • 要用while,而不是if判斷,是否目前線程應該列印輸出
  • 要在finally裡釋放鎖,保證其它的線程能擷取到鎖

3. volatile

上一種做法,我們用了同步+全局變量的方式,那麼有沒有什麼更輕量級的做法?

我們可以直接用volatile修飾變量,volatile能保證變量的更改對所有線程可見。

三個線程按順序列印ABC?十二種做法,深入多線程同步通信機制

volatile

  • 代碼
public class ABC3 {

    //判斷是否執行:1表示應該A執行,2表示應該B執行,3表示應該C執行
    private static volatile Integer state = 1;

    public static void printA() {
        //通過循環,hang住線程
        while (state != 1) {
        }
        System.out.println("A");
        state++;
    }

    public static void printB() throws InterruptedException {
        while (state != 2) {
        }
        System.out.println("B");
        state++;
    }

    public static void printC() throws InterruptedException {
        while (state != 3) {
        }
        System.out.println("C");
        state++;
    }

}
           

4. AtomicInteger

除了無鎖的volatile方法,還有沒有什麼輕量級鎖的方法呢?

我們都知道synchronized和lock都屬于悲觀鎖,我們還可以用樂觀鎖來實作。

在Java裡,我們熟悉的原子操作類AtomicInteger就是基于CAS實作的,可以用來保證Integer操作的原子性。

三個線程按順序列印ABC?十二種做法,深入多線程同步通信機制

AtomicInteger

  • 代碼
public class ABC4 {

    //判斷是否執行:1表示應該A執行,2表示應該B執行,3表示應該C執行
    private static AtomicInteger state = new AtomicInteger(1);

    public static void printA() {
        System.out.println("A");
        state.incrementAndGet();
    }

    public static void printB() throws InterruptedException {
        while (state.get() < 4) {
            while (state.get() == 2) {
                System.out.println("B");
                state.incrementAndGet();
            }
        }
    }

    public static void printC() throws InterruptedException {
        while (state.get() < 4) {
            while (state.get() == 3) {
                System.out.println("C");
                state.incrementAndGet();
            }
        }
    }

}
           

5.lock+condition

在Java中,除了Object的wait和notify/notify可以實作等待/通知機制,Condition和Lock配合同樣可以完成等待通知機制。

使用condition.await(),使目前線程進入等待狀态,使用condition.signal()或者condition.signalAll()喚醒等待線程。

  • 代碼
public class ABC5 {
    //可重入鎖
    private final static Lock lock = new ReentrantLock();
    //判斷是否執行:1表示應該A執行,2表示應該B執行,3表示應該C執行
    private static int state = 1;
    //condition對象
    private static Condition a = lock.newCondition();
    private static Condition b = lock.newCondition();
    private static Condition c = lock.newCondition();

    public static void printA() {
        //通過循環,hang住線程
        while (state < 4) {
            try {
                //擷取鎖
                lock.lock();
                //并發情況下,不能用if,要用循環判斷等待條件,避免虛假喚醒
                while (state != 1) {
                    a.await();
                }
                System.out.println("A");
                state++;
                b.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                //要保證不執行的時候,鎖能釋放掉
                lock.unlock();
            }
        }
    }

    public static void printB() throws InterruptedException {
        while (state < 4) {
            try {
                lock.lock();
                //擷取到鎖,應該執行
                while (state != 2) {
                    b.await();
                }
                System.out.println("B");
                state++;
                c.signal();
            } finally {
                lock.unlock();
            }
        }
    }

    public static void printC() throws InterruptedException {
        while (state < 4) {
            try {
                lock.lock();
                while (state != 3) {
                    c.await();
                }
                //擷取到鎖,應該執行
                System.out.println("C");
                state++;
            } finally {
                lock.unlock();
            }
        }
    }

}
           

6.信号量Semaphore

線程間同步,還可以使用信号量Semaphore,信号量顧名思義,多線程協作時完成信号傳遞。

使用acquire()擷取許可,如果沒有可用的許可,線程進入阻塞等待狀态;使用release釋放許可。

三個線程按順序列印ABC?十二種做法,深入多線程同步通信機制

Semaphore

  • 代碼
public class ABC6 {

    private static Semaphore semaphoreB = new Semaphore(0);
    private static Semaphore semaphoreC = new Semaphore(0);

    public static void printA() {
        System.out.println("A");
        semaphoreB.release();
    }

    public static void printB() throws InterruptedException {
        semaphoreB.acquire();
        System.out.println("B");
        semaphoreC.release();
    }

    public static void printC() throws InterruptedException {
        semaphoreC.acquire();
        System.out.println("C");
    }

}
           

7.計數器CountDownLatch

CountDownLatch的一個适用場景,就是用來進行多個線程的同步管理,線程調用了countDownLatch.await()之後,需要等待countDownLatch的信号countDownLatch.countDown(),在收到信号前,它不會往下執行。

三個線程按順序列印ABC?十二種做法,深入多線程同步通信機制

CountDownLatch

public class ABC7 {

    private static CountDownLatch countDownLatchB = new CountDownLatch(1);
    private static CountDownLatch countDownLatchC = new CountDownLatch(1);

    public static void printA() {
        System.out.println("A");
        countDownLatchB.countDown();
    }

    public static void printB() throws InterruptedException {
        countDownLatchB.await();
        System.out.println("B");
        countDownLatchC.countDown();
    }

    public static void printC() throws InterruptedException {
        countDownLatchC.await();
        System.out.println("C");
    }

}
           

8. 循環栅欄CyclicBarrier

用到了CountDownLatch,我們應該想到,還有一個功能和它類似的工具類CyclicBarrier。

有的翻譯叫同步屏障,我覺得翻譯成循環栅欄,更能展現它的功能特性。

就像是出去旅遊,大家不同時間到了景區門口,但是景區疫情限流,先把栅欄拉下來,在景區裡的遊客走一批,打開栅欄,再放進去一批,走一批,再放進去一批……

這就是CyclicBarrier的兩個特性,

  • 栅欄:多個線程互相等待,到齊後再執行特定動作
  • 循環:所有線程釋放後,還能繼續複用它

這道題怎麼用CyclicBarrier解決呢?

  • 線程B和線程C需要使用栅欄等待
  • 為了讓B和C也順序執行,需要用一個狀态,來辨別應該執行的線程
三個線程按順序列印ABC?十二種做法,深入多線程同步通信機制

CyclicBarrier

  • 代碼
public class ABC8 {

    private static CyclicBarrier cyclicBarrier = new CyclicBarrier(1);
    private static Integer state = 1;

    public static void printA() {
        while (state != 1) {
        }
        System.out.println("A");
        state = 2;
    }

    public static void printB() throws InterruptedException {
        try {
            //在栅欄前等待
            cyclicBarrier.await();
            //state不等于2的時候等待
            while (state != 2) {
            }
            System.out.println("B");
            state = 3;
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }
    }

    public static void printC() throws InterruptedException {
        try {
            cyclicBarrier.await();
            while (state != 3) {
            }
            System.out.println("C");
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }
    }

}

           

當然,CyclicBarrier的實作其實還是基于lock+condition,多個線程在到達一定條件前await,到達條件後signalAll。

9.交換器Exchanger

在前面,我們已經用到了常用的并發工具類,其實還有一個不那麼常用的并發工具類Exchanger,同樣也可以用來解決這道題目。

Exchanger用于兩個線程在某個節點時進行資料交換,在這道題裡:

  • 線程A執行完之後,和線程B用一個交換器交換state,線程B執行完之後,和線程C用一個交換器交換state
  • 在沒有輪到自己執行之前,先進行等待
三個線程按順序列印ABC?十二種做法,深入多線程同步通信機制

Exchanger

public class ABC9 {
    private static Exchanger<Integer> exchangerB = new Exchanger<>();
    private static Exchanger<Integer> exchangerC = new Exchanger<>();

    public static void printA() {
        System.out.println("A");
        try {
            //交換
            exchangerB.exchange(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void printB() {
        try {
            //交換
            Integer state = exchangerB.exchange(0);
            //等待
            while (state != 2) {
            }
            //執行
            System.out.println("B");
            //第二次交換
            exchangerC.exchange(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void printC() {
        try {
            Integer state = exchangerC.exchange(0);
            while (state != 3) {
            }
            System.out.println("C");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

           

Exchanger是基于ThreadLocal實作的,那麼我們這個問題可以基于ThreadLocal來實作嗎?

10.ThreadLocal

ThreadLocal,我們應該都了解過它的用法和原理,那麼怎麼用ThreadLocal實作三個線程順序列印ABC呢?

子線程是并發執行的,但是主線程的代碼是順序執行的,我們在主線程裡改變變量,子線程根據變量判斷。

那麼問題來了,子線程怎麼擷取主線程的變量呢?可以用InheritableThreadLocal。

三個線程按順序列印ABC?十二種做法,深入多線程同步通信機制

ThreadLocal

  • 代碼
public class ABC10 {
    
    public static void main(String[] args) {
        //使用ThreadLocal存儲變量
        ThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();
        threadLocal.set(1);
        new Thread(() -> {
            System.out.println("A");
        }, "A").start();
        //設定變量值
        threadLocal.set(2);

        new Thread(() -> {
            //等待
            while (threadLocal.get() != 2) {
            }
            System.out.println("B");
        }, "B").start();
        threadLocal.set(3);

        new Thread(() -> {
            while (threadLocal.get() != 3) {
            }
            System.out.println("C");
        }, "C").start();
    }
}
           

11.管道流PipedStream

線程之間通信,還有一種比較笨重的辦法——PipedInputStream/PipedOutStream。

一個線程使用PipedOutStream寫資料,一個線程使用PipedInputStream讀資料,而且Piped的讀取隻能一對一。

那麼,在這道題裡:

  • 線程A使用PipedOutStream向線程B寫入資料,線程B讀取後,列印輸出
  • 線程B和C也是相同的姿勢
三個線程按順序列印ABC?十二種做法,深入多線程同步通信機制

管道流

  • 代碼
public class ABC11 {
    public static void main(String[] args) throws IOException {
        //線程A的輸出流
        PipedOutputStream outputStreamA = new PipedOutputStream();
        //線程B的輸出流
        PipedOutputStream outputStreamB = new PipedOutputStream();
        //線程B的輸入流
        PipedInputStream inputStreamB = new PipedInputStream();
        //線程C的輸入流
        PipedInputStream inputStreamC = new PipedInputStream();


        outputStreamA.connect(inputStreamB);
        outputStreamB.connect(inputStreamC);

        new Thread(() -> {
            System.out.println("A");
            try {
                //流寫入
                outputStreamA.write("B".getBytes());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }, "A").start();

        new Thread(() -> {
            //流讀取
            byte[] buffer = new byte[1];
            try {
                inputStreamB.read(buffer);
                //轉換成String
                String msg = new String(buffer);
                System.out.println(msg);
                outputStreamB.write("C".getBytes());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }, "B").start();

        new Thread(() -> {
            byte[] buffer = new byte[1];
            try {
                inputStreamC.read(buffer);
                String msg = new String(buffer);
                System.out.println(msg);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }, "C").start();
    }
}
           

12.阻塞隊列BlockingQueue

阻塞隊列同樣也可以用來進行線程排程。

  • 利用隊列的長度,來确定執行者
  • 利用隊列的阻塞性,來保證入隊操作同步執行。
三個線程按順序列印ABC?十二種做法,深入多線程同步通信機制

阻塞隊列

  • 代碼
public class ABC12 {

    private static BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);

    public static void printA() {
        System.out.println("A");
        queue.offer("B");
    }

    public static void printB() throws InterruptedException {
        while (queue.size() != 1) {
        }
        System.out.println("B");
        queue.offer("C");
    }

    public static void printC() throws InterruptedException {
        while (queue.size() != 2) {
        }
        System.out.println("C");
    }

}

           

總結

這篇文章給大家帶來了三個線程順序列印ABC的的十二種做法,裡面有些寫法肯定是備援的,大家有沒有什麼更好的寫法呢?

通過十二種題解,我們基本上把Java并發中主要的線程同步和通信方式過了一遍,相信通過這道題的實踐,我們也能對Java線程的同步和通信有更深的了解。

最後,也給大家留兩道“進階”一點的題目,感興趣可以自己實作一下:

  • 兩個線程,一個線程列印奇數,一個線程列印偶數
  • 按照順序,三個線程分别列印A5次,B10次,C15次

原文:https://mp.weixin.qq.com/s/ughf9vl2iDZQ20mJMlrIBw

作者:三分惡

如果感覺本文對你有幫助,點贊關注支援一下

繼續閱讀