多執行緒知識:三個執行緒如何交替列印ABC迴圈100次

2023-07-07 18:01:00

本文博主給大家講解一道網上非常經典的多執行緒面試題目。關於三個執行緒如何交替列印ABC迴圈100次的問題。

下文實現程式碼都基於Java程式碼在單個JVM內實現。

問題描述

給定三個執行緒,分別命名為A、B、C,要求這三個執行緒按照順序交替列印ABC,每個字母列印100次,最終輸出結果為:

A
B
C
A
B
C
...
A
B
C

推薦博主開源的 H5 商城專案waynboot-mall,這是一套全部開源的微商城專案,包含三個專案:運營後臺、H5 商城前臺和伺服器端介面。實現了商城所需的首頁展示、商品分類、商品詳情、商品 sku、分詞搜尋、購物車、結算下單、支付寶/微信支付、收單評論以及完善的後臺管理等一系列功能。 技術上基於最新得 Springboot3.0、jdk17,整合了 MySql、Redis、RabbitMQ、ElasticSearch 等常用中介軟體。分模組設計、簡潔易維護,歡迎大家點個 star、關注博主。

github 地址:https://github.com/wayn111/waynboot-mall

解決思路

這是一個典型的多執行緒同步的問題,需要保證每個執行緒在列印字母之前,能夠判斷是否輪到自己執行,以及在列印字母之後,能夠通知下一個執行緒執行。為了實現這一目標,博主講介紹以下5種方法:

  • 使用synchronized和wait/notify
  • 使用ReentrantLock和Condition
  • 使用Semaphore
  • 使用AtomicInteger和CAS
  • 使用CyclicBarrier

方法一:使用synchronized和wait/notify

synchronized是Java中的一個關鍵字,用於實現對共用資源的互斥存取。wait和notify是Object類中的兩個方法,用於實現執行緒間的通訊。wait方法會讓當前執行緒釋放鎖,並進入等待狀態,直到被其他執行緒喚醒。notify方法會喚醒一個在同一個鎖上等待的執行緒。

我們可以使用一個共用變數state來表示當前應該列印哪個字母,初始值為0。當state為0時,表示輪到A執行緒列印;當state為1時,表示輪到B執行緒列印;當state為2時,表示輪到C執行緒列印。每個執行緒在列印完字母后,需要將state加1,並對3取模,以便迴圈。同時,每個執行緒還需要喚醒下一個執行緒,並讓自己進入等待狀態。

具體的程式碼實現如下:

public class PrintABC {

    // 共用變數,表示當前應該列印哪個字母
    private static int state = 0;

    // 共用物件,作為鎖和通訊的媒介
    private static final Object lock = new Object();

    public static void main(String[] args) {
        // 建立三個執行緒
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    // 迴圈100次
                    for (int i = 0; i < 100; i++) {
                        // 獲取鎖
                        synchronized (lock) {
                            // 判斷是否輪到自己執行
                            while (state % 3 != 0) {
                                // 不是則等待
                                lock.wait();
                            }
                            // 列印字母
                            System.out.println("A");
                            // 修改狀態
                            state++;
                            // 喚醒下一個執行緒
                            lock.notifyAll();
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 100; i++) {
                        synchronized (lock) {
                            while (state % 3 != 1) {
                                lock.wait();
                            }
                            System.out.println("B");
                            state++;
                            lock.notifyAll();
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread threadC = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 100; i++) {
                        synchronized (lock) {
                            while (state % 3 != 2) {
                                lock.wait();
                            }
                            System.out.println("C");
                            state++;
                            lock.notifyAll();
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // 啟動三個執行緒
        threadA.start();
        threadB.start();
        threadC.start();
    }
}

方法二:使用ReentrantLock和Condition

ReentrantLock是Java中的一個類,用於實現可重入的互斥鎖。Condition是ReentrantLock中的一個介面,用於實現執行緒間的條件等待和喚醒。ReentrantLock可以建立多個Condition物件,每個Condition物件可以繫結一個或多個執行緒,實現對不同執行緒的精確控制。

我們可以使用一個ReentrantLock物件作為鎖,同時建立三個Condition物件,分別繫結A、B、C三個執行緒。每個執行緒在列印字母之前,需要呼叫對應的Condition物件的await方法,等待被喚醒。每個執行緒在列印字母之後,需要呼叫下一個Condition物件的signal方法,喚醒下一個執行緒。

具體的程式碼實現如下:

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

public class PrintABC {

    // 共用變數,表示當前應該列印哪個字母
    private static int state = 0;

    // 可重入鎖
    private static final ReentrantLock lock = new ReentrantLock();

    // 三個條件物件,分別繫結A、B、C三個執行緒
    private static final Condition A = lock.newCondition();
    private static final Condition B = lock.newCondition();
    private static final Condition C = lock.newCondition();

    public static void main(String[] args) {
        // 建立三個執行緒
        Thread threaA = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    // 迴圈100次
                    for (int i = 0; i < 100; i++) {
                        // 獲取鎖
                        lock.lock();
                        try {
                            // 判斷是否輪到自己執行
                            while (state % 3 != 0) {
                                // 不是則等待
                                A.await();
                            }
                            // 列印字母
                            System.out.println("A");
                            // 修改狀態
                            state++;
                            // 喚醒下一個執行緒
                            B.signal();
                        } finally {
                            // 釋放鎖
                            lock.unlock();
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread threaB = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 100; i++) {
                        lock.lock();
                        try {
                            while (state % 3 != 1) {
                                B.await();
                            }
                            System.out.println("B");
                            state++;
                            C.signal();
                        } finally {
                            lock.unlock();
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread threaC = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 100; i++) {
                        lock.lock();
                        try {
                            while (state % 3 != 2) {
                                C.await();
                            }
                            System.out.println("C");
                            state++;
                            A.signal();
                        } finally {
                            lock.unlock();
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // 啟動三個執行緒
        threaA.start();
        threaB.start();
        threaC.start();
    }
}

方法三:使用Semaphore

Semaphore是Java中的一個類,用於實現號誌機制。號誌是一種計數器,用於控制對共用資源的存取。Semaphore可以建立多個號誌物件,每個號誌物件可以繫結一個或多個執行緒,實現對不同執行緒的精確控制。

我們可以使用三個Semaphore物件,分別初始化為1、0、0,表示A、B、C三個執行緒的初始許可數。每個執行緒在列印字母之前,需要呼叫對應的Semaphore物件的acquire方法,獲取許可。每個執行緒在列印字母之後,需要呼叫下一個Semaphore物件的release方法,釋放許可。

具體的程式碼實現如下:

import java.util.concurrent.Semaphore;

public class PrintABC {
    private static int state = 0;

    // 三個號誌物件,分別表示A、B、C三個執行緒的初始許可數
    private static final Semaphore A = new Semaphore(1);
    private static final Semaphore B = new Semaphore(0);
    private static final Semaphore C = new Semaphore(0);

    public static void main(String[] args) {
        // 建立三個執行緒
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    // 迴圈100次
                    for (int i = 0; i < 100; i++) {
                        // 獲取許可
                        A.acquire();
                        // 列印字母
                        System.out.println("A");
                        // 修改狀態
                        state++;
                        // 釋放許可
                        B.release();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 100; i++) {
                        B.acquire();
                        System.out.println("B");
                        state++;
                        C.release();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread threadC = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 100; i++) {
                        C.acquire();
                        System.out.println("C");
                        state++;
                        A.release();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // 啟動三個執行緒
        threadA.start();
        threadB.start();
        threadC.start();
    }
}

方法四:使用AtomicInteger和CAS

AtomicInteger是Java中的一個類,用於實現原子性的整數操作。CAS是一種無鎖的演演算法,全稱為Compare And Swap,即比較並交換。CAS操作需要三個引數:一個記憶體地址,一個期望值,一個新值。如果記憶體地址的值與期望值相等,就將其更新為新值,否則不做任何操作。

我們可以使用一個AtomicInteger物件來表示當前應該列印哪個字母,初始值為0。當state為0時,表示輪到A執行緒列印;當state為1時,表示輪到B執行緒列印;當state為2時,表示輪到C執行緒列印。每個執行緒在列印完字母后,需要使用CAS操作將state加1,並對3取模,以便迴圈。

具體的程式碼實現如下:

import java.util.concurrent.atomic.AtomicInteger;

public class PrintABC {

    // 共用變數,表示當前應該列印哪個字母
    private static AtomicInteger state = new AtomicInteger(0);

    public static void main(String[] args) {
        // 建立三個執行緒
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                // 迴圈100次
                for (int i = 0; i < 100; ) {
                    // 判斷是否輪到自己執行
                    if (state.get() % 3 == 0) {
                        // 列印字母
                        System.out.println("A");
                        // 修改狀態,使用CAS操作保證原子性
                        state.compareAndSet(state.get(), state.get() + 1);
                        // 計數器加1
                        i++;
                    }
                }
            }
        });

        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; ) {
                    if (state.get() % 3 == 1) {
                        System.out.println("B");
                        state.compareAndSet(state.get(), state.get() + 1);
                        i++;
                    }
                }
            }
        });

        Thread threadC = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; ) {
                    if (state.get() % 3 == 2) {
                        System.out.println("C");
                        state.compareAndSet(state.get(), state.get() + 1);
                        i++;
                    }
                }
            }
        });

        // 啟動三個執行緒
        threadA.start();
        threadB.start();
        threadC.start();
    }
}

方法五:使用CyclicBarrier

CyclicBarrier是Java中的一個類,用於實現多個執行緒之間的屏障。CyclicBarrier可以建立一個屏障物件,指定一個參與等待執行緒數和一個到達屏障點時得動作。當所有執行緒都到達屏障點時,會執行屏障動作,然後繼續執行各自的任務。CyclicBarrier可以重複使用,即當所有執行緒都通過一次屏障後,可以再次等待所有執行緒到達下一次屏障。

我們可以使用一個CyclicBarrier物件,指定三個執行緒為參與等待數,以及一個列印字母的到達屏障點動作。每個執行緒在執行完自己的任務後,需要呼叫CyclicBarrier物件的await方法,等待其他執行緒到達屏障點。當所有執行緒都到達屏障點時,會執行列印字母的屏障動作,並根據state的值判斷應該列印哪個字母。然後,每個執行緒繼續執行自己的任務,直到迴圈結束。需要注意得就是由於列印操作在到達屏障點得動作內執行,所以三個執行緒得迴圈次數得乘以參與執行緒數量,也就是三。

具體的程式碼實現如下:

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

public class PrintABC {

    // 共用變數,表示當前應該列印哪個字母
    private static int state = 0;

    // 參與執行緒數量
    private static int threadNum = 3;

    // 迴圈屏障,指定三個執行緒為屏障點,以及一個列印字母的屏障動作
    private static final CyclicBarrier barrier = new CyclicBarrier(threadNum, new Runnable() {
        @Override
        public void run() {
            // 根據state的值判斷應該列印哪個字母
            switch (state) {
                case 0:
                    System.out.println("A");
                    break;
                case 1:
                    System.out.println("B");
                    break;
                case 2:
                    System.out.println("C");
                    break;
            }
            // 修改狀態
            state = (state + 1) % 3;
            System.out.println(state);
        }
    });

    public static void main(String[] args) {
        // 建立三個執行緒
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    // 迴圈100次
                    for (int i = 0; i < threadNum * 100; i++) {
                        // 執行自己的任務
                        // ...
                        // 等待其他執行緒到達屏障點
                        barrier.await();
                    }
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    for (int i = 0; i < threadNum * 100; i++) {
                        // 執行自己的任務
                        // ...
                        // 等待其他執行緒到達屏障點
                        barrier.await();
                    }
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread threadC = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    for (int i = 0; i < threadNum * 100; i++) {
                        // 執行自己的任務
                        // ...
                        // 等待其他執行緒到達屏障點
                        barrier.await();
                    }
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }
        });

        // 啟動三個執行緒
        threadA.start();
        threadB.start();
        threadC.start();
    }
}

總結

到此,本文內容已經講解完畢,以上的這五種方法都可以利用不同的工具和機制來實現多執行緒之間的同步和通訊,從而保證按照順序交替列印ABC。這些方法各有優缺點,具體的選擇需要根據實際的場景和需求來決定。

最後本文講解程式碼是在單個JVM內的實現方法,如果大家對涉及到多個JVM來實現按照順序交替列印ABC的話,可以私信博主,博主再給大家出一期文章進行講解。

關注公眾號【waynblog】每週分享技術乾貨、開源專案、實戰經驗、高效開發工具等,您的關注將是我的更新動力!