多線程知識:三個線程如何交替列印ABC迴圈100次

来源:https://www.cnblogs.com/waynaqua/archive/2023/07/07/17535232.html
-Advertisement-
Play Games

本文博主給大家講解一道網上非常經典的多線程面試題目。關於三個線程如何交替列印ABC迴圈100次的問題。 > 下文實現代碼都基於Java代碼在單個JVM內實現。 ## 問題描述 給定三個線程,分別命名為A、B、C,要求這三個線程按照順序交替列印ABC,每個字母列印100次,最終輸出結果為: ``` A ...


本文博主給大家講解一道網上非常經典的多線程面試題目。關於三個線程如何交替列印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】每周分享技術乾貨、開源項目、實戰經驗、高效開發工具等,您的關註將是我的更新動力!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 只要將配置信息存放在與源代碼不同的存儲庫中,將其鎖好,僅對有權訪問的人開放,並且管理員能夠根據過程、程式和執行人等授予或撤銷對相關配置信息的訪問許可權,那麼配置信息也可以存放在版本控制系統中 ...
  • ### 構造器參數 - maxFrameLength:指定解碼器所能處理的數據包的最大長度,超過該長度則拋出 TooLongFrameException 異常; - lengthFieldOffset:指定長度欄位的起始位置; - lengthFieldLength:指定長度欄位的長度:目前支持1( ...
  • # 概述 NumPy是一個開源的科學計算庫,它提供了高效的數值計算和數組操作功能,主要包括: * 多維數組的創建、操作和索引。 * 數組的切片、拼接和轉置。 * 數組的乘法、除法、求導、積分、對數等基本運算。 * 數組的逐元素操作、求平均值、中位數、眾數等統計量。 * 數組作為列表、元組等數據類型進 ...
  • 在Mac OS上安裝vs code的java開發環境. 按照vs code的官方說明安裝Java相關插件, 遇見下列問題並解決了. 安裝JDK環境 安裝Extension Pack for Java 插件後,vscode會提示你安裝一個java,我安裝提示安裝了java.後來才發現安裝的是jre,並 ...
  • 相信閱讀過上期文章,動手能力強的朋友們已經自己跑出來界面了。所以這期我要講的是交互部分,也就是對於滑鼠點擊事件的響應,包括計時計數對點擊事件以及一些狀態量的影響。 回憶下第一期介紹的掃雷規則和操作,游戲從開局到結束可能會涉及到哪些情況呢?我認為比較重要的就是明確什麼情況下游戲已經結束,結束代表的是勝 ...
  • # python multiprocessing庫使用記錄 需求是想並行調用形式化分析工具proverif,同時發起對多個query的分析(378個)。實驗室有40核心80線程的伺服器(雙cpu,至強gold 5218R*2)。 觀察到單個命令在分析時記憶體占用不大,且只使用單核心執行,因此考慮同時調 ...
  • 首先聲明,我不是小黑子,我不是小黑子! 作為一個ikun,時刻都在想著我們家姐姐! 這不上次用Python做了一個ikun飛機大戰,今天再給大家整活一手,Python tkinter開發一個專屬ikun音樂播放器,這樣就能時刻看到姐姐了。 咱們來看看效果 代碼實現 今天要做的就是上面的簡易音樂播放器 ...
  • # 一、編譯和鏈接的過程 ## 1、GCC生成可執行文件的總體過程 在日常的開發過程中,IDE總是會幫我們將編譯和鏈接合併,一鍵式的執行,即使在liunx中,使用命令行來編譯一個源文件也只是簡單的一句"gcc hello.c"。我們並沒有過多的關註編譯和鏈接的運行機制和機理,我想從本質出發,深入瞭解 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...