總結:實現線程同步的八種方式

来源:https://www.cnblogs.com/chengxyyh/archive/2020/07/03/13230913.html
-Advertisement-
Play Games

前言: 在多線程中線程的執行順序是依靠哪個線程先獲得到CUP的執行權誰就先執行,雖然說可以通過線程的優先權進行設置,但是他只是獲取CUP執行權的概率高點,但是也不一定必須先執行。在這種情況下如何保證線程按照一定的順序進行執行,今天就來一個大總結,分別介紹一下幾種方式。 一、通過Object的wait ...


前言:

在多線程中線程的執行順序是依靠哪個線程先獲得到CUP的執行權誰就先執行,雖然說可以通過線程的優先權進行設置,但是他只是獲取CUP執行權的概率高點,但是也不一定必須先執行。在這種情況下如何保證線程按照一定的順序進行執行,今天就來一個大總結,分別介紹一下幾種方式。
一、通過Object的wait和notify
二、通過Condition的awiat和signal
三、通過一個阻塞隊列
四、通過兩個阻塞隊列
五、通過SynchronousQueue 
六、通過線程池的Callback回調
七、通過同步輔助類CountDownLatch
八、通過同步輔助類CyclicBarrier

一、通過Object的wait和notify

寫一個測試了Test,加上main方法,在寫一個內部類Man進行測試。main方法如下,他進行創建兩個線程,傳進去Runnable對象。

public static boolean flag = false;

public static int num = 0;

public static void main(String[] args) {
    Man man = new Man();

    new Thread(() -> {
        man.getRunnable1();
    }).start();
    new Thread(() -> {
        man.getRunnable2();
    }).start();
}

getRunnable1和getRunnable2分別表示兩個需要執行的任務,在兩個線程中進行,方法1用於數據的生產,方法二用於數據的獲取,數據的初始值為num = 0,為了保證生產和獲取平衡需要使用wait和notify方法,這兩個方法的使用必須是要加鎖的,因此使用synchronized進行加鎖使用,為了演示這個效果,我們加上一個sleep方法模擬處理時間,如下:

public static class Man {
    
    public synchronized void getRunnable1() {
        for (int i = 0; i < 20; i++) {
            while (flag) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("生產出:" + (++num) + "個");
            flag = true;
            notify();
        }
    }
    
    public synchronized void getRunnable2() {
        for (int i = 0; i < 20; i++) {
            while (!flag) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            //模擬載入時間
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("取出出:" + (num--) + "個");
            System.out.println("------------------");

            flag = false;
            notify();
        }
    }
}

分析它的載入流程,從方法1進行分析,由於flag的初始條件為false,所以方法1不進入等待,直接進行生產,生產完成成之後,更新flag的值為true,同時notify下一個方法2的wait方法,使其變為喚醒狀態。這時候由於方法1加鎖了,無法執行方法1其他部分,當方法1執行完畢,方法1才有可能執行,但是方法1的flag已經為true,進入到wait裡面又處於阻塞狀態,所以這時候只能執行方法2了。由於方法2被喚醒了,阻塞解除,接下來就獲取數據,當獲取完畢又再次讓flag變為false,notify方法1解除阻塞,再次執行方法1,就這樣不斷的迴圈,保證了不同線程的有序執行,直到程式終止。

運行效果如下:

二、通過Condition的awiat和signal

上面第一個的實現是一個阻塞,一個等待的方式保證線程有序的執行,但是不能進行兩個線程之間進行通信,而接下來介紹的Condition就具備這樣的功能。要獲取Condition對象首先先得獲取Lock對象,他是在jdk1.5之後增加的,比synchronized性能更好的一種鎖機制。和上面的類似,拷貝一份代碼,看看main方法:

public static boolean flag = false;

public static int num = 0;

public static void main(String[] args) {
    Man man = new Man();

    new Thread(() -> {
        man.getRunnable1();
    }).start();
    new Thread(() -> {
        man.getRunnable2();
    }).start();
}

情況和第一個實現方法分析一致,這裡不重覆了。主要看內部類Man中的方法1和方法2。先手創建鎖對象,把synchronized改為使用Lock加鎖,其次通過Lock創建Condition對象,替換掉Object類的wait方法為Condition的await方法,最後換掉notify方法為signal方法即可,執行原理和上面分析一致,代碼如下:

public static class Man {
    public static ReentrantLock lock = new ReentrantLock();
    public static Condition condition = lock.newCondition();

    public void getRunnable1() {
        lock.lock();
        try {
            for (int i = 0; i < 20; i++) {
                while (flag) {
                    try {
                        condition.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("生產出:" + (++num) + "個");
                flag = true;
                condition.signal();
            }
        } finally {
            lock.lock();
        }
    }

    public void getRunnable2() {
        lock.lock();
        try {
            for (int i = 0; i < 20; i++) {
                while (!flag) {
                    try {
                        condition.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("取出出:" + (num--) + "個");
                System.out.println("------------------");
                flag = false;
                condition.signal();
            }
        } finally {
            lock.unlock();
        }
    }
}

執行結果如下:

這是我的iOS開發交流群:519832104不管你是小白還是大牛歡迎入駐,可以一起分享經驗,討論技術,共同學習成長!
另附上一份各好友收集的大廠面試題,需要iOS開發學習資料、面試真題,進群即可自行下載!

點擊此處,立即與iOS大牛交流學習

三、通過一個阻塞隊列

上面的兩個方法實現起來代碼比較繁瑣,如果通過阻塞隊列來實現會更加簡潔,這裡採用常用的容量為64的ArrayBlockingQueue來實現。main方法如下:

public static void main(String[] args) {
    Man man = new Man();

    new Thread(() -> {
        man.getRunnable1();
    }).start();
    new Thread(() -> {
        man.getRunnable2();
    }).start();
}

主要來看Man中的方法1和方法2,方法1中生產數據,這裡把生產的數據存進隊列裡面,同時方法2進行取數據,如果方法1放滿了或者方法2取完了就會被阻塞住,等待方法1生產好了或者方法2取出了,然後再進行。代碼如下:

public static class Man {

    ArrayBlockingQueue queue = new ArrayBlockingQueue<Integer>(64);

    public void getRunnable1() {
        for (int i = 0; i < 8; i++) {
            System.out.println("生產出:" + i + "個");
            try {
                queue.put(i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("---------------生產完畢-----------------");
    }

    public void getRunnable2() {
        for (int i = 0; i < 8; i++) {
            try {
                int num = (int) queue.take();
                System.out.println("取出出:" + num);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

很明顯使用阻塞隊列代碼精煉了很多,在這還可以發現這個阻塞隊列是具有緩存功能的,想很多Android中網路訪問框架內部就是使用這個進行緩存的,例如Volley、Okhttp等等。

運行效果如下:

四、通過兩個阻塞隊列

使用一個阻塞隊列能夠實現線程同步的功能,兩個阻塞隊列也可以實現線程同步。原理是ArrayBlockingQueue他是具有容量的,如果把他的容量定位1則意味著他只能放進去一個元素,第二個方進行就會就會被阻塞。按照這個原理進行來實現,定義兩個容量為1的阻塞隊列ArrayBlockingQueue,一個存放數據,另一個用於控制次序。main方法和上面一致,主要來看看Man類中的兩個方法:

static class Man {
    //數據的存放
    ArrayBlockingQueue queue1 = new ArrayBlockingQueue<Integer>(1);
    //用於控製程序的執行
    ArrayBlockingQueue queue2 = new ArrayBlockingQueue<Integer>(1);

    {
        try {
            //queue2放進去一個元素,getRunnable2阻塞
            queue2.put(22222);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void getRunnable1() {
        new Thread(() -> {
            for (int j = 0; j < 20; j++) {
                try {
                    //queue1放進一個元素,getRunnable1阻塞

                    queue1.put(j);
                    System.out.println("存放   線程名稱:" + Thread.currentThread().getName() + "-數據為-" + j);

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                try {
                    //queue2取出元素,getRunnable2進入
                    queue2.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    public void getRunnable2() {
        new Thread(() -> {
            for (int j = 0; j < 20; j++) {
                try {
                    //queue2放進一個元素,getRunnable2阻塞
                    queue2.put(22222);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                try {
                    //queue1放進一個元素,getRunnable1進入

                    int i = (int) queue1.take();
                    System.out.println("獲取   線程名稱:" + Thread.currentThread().getName() + "-數據為-" + i);

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

再次提醒queue2用於控製程序的執行次序,並無實際含義。最後看看運行效果,存一個、取一個很清晰,如下:

五、通過SynchronousQueue

SynchronousQueue不同於一般的數據等線程,而是線程等待數據,他是一個沒有數據緩衝的BlockingQueue,生產者線程對其的插入操作put必須等待消費者的移除操作take,反過來也一樣。通過這一特性來實現一個多線程同步問題的解決方案,代碼如下:

/**
 * 使用阻塞隊列SynchronousQueue
 * offer將數據插入隊尾
 * take取出數據,如果沒有則阻塞,直到有數據在獲取到
 */
public static void test() {
    SynchronousQueue queue = new SynchronousQueue();
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    executorService.execute(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(5000);
                queue.offer(9);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    try {
        int take = (int) queue.take();
        System.out.println(take);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

子線程中進行設置數據,而主線程獲取數據,如果子線程沒執行完畢,子線程沒有執行完畢主線程就會被阻塞住不能執行下一步。

六、通過線程池的Callback回調

線上程的創建中,有一種創建方法可以返回線程結果,就是callback,他能返回線程的執行結果,通過子線程返回的結果進而在主線程中進行操作,也是一種同步方法,這種同步在Android中特別適用,例如Android中的AsyncTask源碼中任務的創建部分。代碼如下:

private static void test() {
    ExecutorService executorService = Executors.newFixedThreadPool(5);
    Future<Boolean> submit = executorService.submit(new Callable<Boolean>() {
        @Override
        public Boolean call() throws Exception {
            return false;
        }
    });
    try {
        if (submit.get()) {
            System.out.println(true);
        } else {
            System.out.println(false);
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
}

七、通過同步輔助類CountDownLatch

CountDownLatch是一個同步的輔助類,允許一個或多個線程,等待其他一組線程完成操作,再繼續執行。他類實際上是使用計數器的方式去控制的,在創建的時候傳入一個int數值每當我們調用countDownt()方法的時候就使得這個變數的值減1,而對於await()方法則去判斷這個int的變數的值是否為0,是則表示所有的操作都已經完成,否則繼續等待。可以理解成倒計時鎖。

public class Test7 {
    public static void main(String[] args) {
        //啟動兩個線程,分別執行完畢之後再執行主線程
        CountDownLatch countDownLatch = new CountDownLatch(2);
 
        //線程1執行
        Thread thread1 = new Thread(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "線程執行完畢");
            countDownLatch.countDown();
        });
        //線程2執行
        Thread thread2 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "線程執行完畢");
            countDownLatch.countDown();
        });
 
 
        thread1.start();
        thread2.start();
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
 
        //執行主線程
        System.out.println("主線程執行完畢");
    }
}

結果如下:

八、通過同步輔助類CyclicBarrier

CyclicBarrier是一個同步的輔助類,和上面的CountDownLatch比較類似,不同的是他允許一組線程相互之間等待,達到一個共同點,再繼續執行。可看成是個障礙,所有的線程必須到齊後才能一起通過這個障礙。

public class Test8 {
    public static void main(String[] args) {
        //啟動兩個線程,分別執行完畢之後再執行主線程
        CyclicBarrier barrier  = new CyclicBarrier(2, () -> {
            //執行主線程
            System.out.println("主線程執行完畢");
 
        });
 
        //線程1執行
        Thread thread1 = new Thread(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
 
            System.out.println(Thread.currentThread().getName() + "線程執行完畢");
 
            try {
                barrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        });
 
        //線程2執行
        Thread thread2 = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "線程執行完畢");
            try {
                barrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        });
 
 
        thread1.start();
        thread2.start();
    }
}

運行結果:

查看更多: iOS面試題合集


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

-Advertisement-
Play Games
更多相關文章
  • 7月2日消息:近2.3萬個MongoDB資料庫遭黑客攻擊勒索特幣贖金,幾乎占MongoDB資料庫的47%。黑客要求每個資料庫2天內支付0.015 BTC(約合140美元),否則泄露其數據。如何解決此類數據安全問題?本文為您帶來“資料庫遭比特幣勒索的解密與恢復實戰”。 ...
  • MySQL調優 優化需要考慮哪些方面 優化目標與方向定位 總體目標:使得響應時間更快,吞吐量更大。 (throughout 吞吐量:單位時間內處理事務的數量) 如何找到需要優化的地方 使用反饋。比如做出一些操作後導致效率降低 分析日誌。 監控伺服器資源。系統,記憶體,I/O 監控資料庫運行狀況 可優化 ...
  • 原理: Redis集群採用一致性哈希槽的方式將集群中每個主節點都分配一定的哈希槽,對寫入的數據進行哈希後分配到某個主節點進行存儲。 集群使用公式(CRC16 key)& 16384計算鍵key數據那個槽。 16384個slot均勻分佈在各個節點上。 集群中每個主節點將承擔一部分槽點的維護,而槽點中存 ...
  • 準備4台虛擬機,安裝好ol7.7,分配固定ip192.168.168.11 12 13 14,其中192.168.168.11作為master,其他3個作為slave,主節點也同時作為namenode的同時也是datanode,192.168.168.14作為datanode的同時也作為second... ...
  • 本文更新於2019-06-22,使用MySQL 5.7,操作系統為Deepin 15.4。 為了便於描述,此處將創建視圖的DDL覆述一次,其已於“SQL”章節描述。 CREATE [OR REPLACE] [ALGORITHM={UNDEFINED|MERGE|TEMPTABLE}] VIEW vi ...
  • Plink是一個基於Flink的流處理平臺,旨在基於 [Apache Flink]封裝構建上層平臺。 提供常見的作業管理功能。如作業的創建,刪除,編輯,更新,保存,啟動,停止,重啟,管理,多作業模板配置等。 Flink SQL 編輯提交功能。如 SQL 的線上開發,智能提示,格式化,語法校驗,保存, ...
  • 前言 閑暇之時,羚羊給大家分享一下羚羊在Centos7 下安裝Cloudera Manager 6.3.0和cloudera cdh 6.3.2的過程和安裝過程中遇到的坑。至於為什麼要選擇CDH,Cloudera Manager和cdh是什麼,之間又是什麼關係,在這裡羚羊就不做介紹了。 為什麼選擇C ...
  • 零基礎入門貪吃蛇游戲 貪吃蛇是一款最常見、最經典、最受歡迎的小游戲之一。本篇文章帶你零基礎實現貪吃蛇游戲,一條蛇的使命從這裡開始。 演示地址:貪吃蛇演示,可能會提示危險操作,請忽略,放心訪問。 1、游戲描述 貪吃蛇是一款非常經典的休閑類游戲。在一塊固定大小的區域內,游戲玩家通過控制貪吃蛇的移動去吃食 ...
一周排行
    -Advertisement-
    Play Games
  • 概述:在C#中,++i和i++都是自增運算符,其中++i先增加值再返回,而i++先返回值再增加。應用場景根據需求選擇,首碼適合先增後用,尾碼適合先用後增。詳細示例提供清晰的代碼演示這兩者的操作時機和實際應用。 在C#中,++i 和 i++ 都是自增運算符,但它們在操作上有細微的差異,主要體現在操作的 ...
  • 上次發佈了:Taurus.MVC 性能壓力測試(ap 壓測 和 linux 下wrk 壓測):.NET Core 版本,今天計劃準備壓測一下 .NET 版本,來測試並記錄一下 Taurus.MVC 框架在 .NET 版本的性能,以便後續持續優化改進。 為了方便對比,本文章的電腦環境和測試思路,儘量和... ...
  • .NET WebAPI作為一種構建RESTful服務的強大工具,為開發者提供了便捷的方式來定義、處理HTTP請求並返迴響應。在設計API介面時,正確地接收和解析客戶端發送的數據至關重要。.NET WebAPI提供了一系列特性,如[FromRoute]、[FromQuery]和[FromBody],用 ...
  • 原因:我之所以想做這個項目,是因為在之前查找關於C#/WPF相關資料時,我發現講解圖像濾鏡的資源非常稀缺。此外,我註意到許多現有的開源庫主要基於CPU進行圖像渲染。這種方式在處理大量圖像時,會導致CPU的渲染負擔過重。因此,我將在下文中介紹如何通過GPU渲染來有效實現圖像的各種濾鏡效果。 生成的效果 ...
  • 引言 上一章我們介紹了在xUnit單元測試中用xUnit.DependencyInject來使用依賴註入,上一章我們的Sample.Repository倉儲層有一個批量註入的介面沒有做單元測試,今天用這個示例來演示一下如何用Bogus創建模擬數據 ,和 EFCore 的種子數據生成 Bogus 的優 ...
  • 一、前言 在自己的項目中,涉及到實時心率曲線的繪製,項目上的曲線繪製,一般很難找到能直接用的第三方庫,而且有些還是定製化的功能,所以還是自己繪製比較方便。很多人一聽到自己畫就害怕,感覺很難,今天就分享一個完整的實時心率數據繪製心率曲線圖的例子;之前的博客也分享給DrawingVisual繪製曲線的方 ...
  • 如果你在自定義的 Main 方法中直接使用 App 類並啟動應用程式,但發現 App.xaml 中定義的資源沒有被正確載入,那麼問題可能在於如何正確配置 App.xaml 與你的 App 類的交互。 確保 App.xaml 文件中的 x:Class 屬性正確指向你的 App 類。這樣,當你創建 Ap ...
  • 一:背景 1. 講故事 上個月有個朋友在微信上找到我,說他們的軟體在客戶那邊隔幾天就要崩潰一次,一直都沒有找到原因,讓我幫忙看下怎麼回事,確實工控類的軟體環境複雜難搞,朋友手上有一個崩潰的dump,剛好丟給我來分析一下。 二:WinDbg分析 1. 程式為什麼會崩潰 windbg 有一個厲害之處在於 ...
  • 前言 .NET生態中有許多依賴註入容器。在大多數情況下,微軟提供的內置容器在易用性和性能方面都非常優秀。外加ASP.NET Core預設使用內置容器,使用很方便。 但是筆者在使用中一直有一個頭疼的問題:服務工廠無法提供請求的服務類型相關的信息。這在一般情況下並沒有影響,但是內置容器支持註冊開放泛型服 ...
  • 一、前言 在項目開發過程中,DataGrid是經常使用到的一個數據展示控制項,而通常表格的最後一列是作為操作列存在,比如會有編輯、刪除等功能按鈕。但WPF的原始DataGrid中,預設只支持固定左側列,這跟大家習慣性操作列放最後不符,今天就來介紹一種簡單的方式實現固定右側列。(這裡的實現方式參考的大佬 ...