java併發編程之線程同步(CountDownLatch、CyclicBarrier)

多線程

線程:類似執行一個命令,多線程:併發執行多條命令。

多線程的優點:
1.充分利用cpu的性能。
2.提高系統性能。
3.同一時刻處理可以處理不同的命令

線程同步

即當有一個線程在對內存進行操作時,其他線程都不可以對這個內存地址進行操作,直到該線程完成操作,爲什麼需要它呢?

1.多線程會出現線程安全問題,線程同步可以有效的保證線程安全。
        2.當主線程依賴兩個子線程結果的時候,需要線程同步

如何實現線程同步?
1.加鎖,如:synchronized。
2.通過wait和和notify(和notifyAll),推薦使用notifyAll。
3.線程池callback。
4.join()。
5.CountDownLatch(java SDK包)。
6.CyclicBarrier(java SDK包)。
等等。。。。。。。。。。。。。。。。

這裏只介紹5、6兩種

我們先來看一個沒有加入線程同步的代碼:

public static  void hello(){
        System.out.println("線程:"+Thread.currentThread().getName()+" 執行了。。。。。。。。。");
    }

    public static void main(String[] args) {
        //線程1
        Thread t1 = new Thread(() -> {
            hello();
        });
        t1.start();
        //線程2
        Thread t2 = new Thread(() -> {
            hello();
        });
        t2.start();
        System.out.println("主函數執行完畢。。。。。。。。。");
    }

打印結果:


main方法的輸出語句居然比兩個子線程先執行,爲什麼呢?因爲main是主線程,t1、t2是兩個子線程,由於線程的執行順序是無序的,所以就會導致每次的執行結果都不相同,現在我想實現當t1、t2執行完成之後在執行main方法的輸出語句,該如何實現呢?只需要給t1、t2分別加一個方法即可:

public static  void hello(){
        System.out.println("線程:"+Thread.currentThread().getName()+" 執行了。。。。。。。。。");
    }

    public static void main(String[] args) {
        //線程1
        Thread t1 = new Thread(() -> {
            hello();
        });
        t1.start();
        //線程2
        Thread t2 = new Thread(() -> {
            hello();
        });
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("主函數執行完畢。。。。。。。。。");
    }

結果如下:

爲什麼join(),可以實現線程同步呢?join()源碼如下:

很明顯,這裏使用while做了循環等待,讓線程不往下執行,達到線程同步(等待)的效果。

然而我們平時的開發過程中基本不會這麼創建線程,一般都是使用線程池,那在使用線程池的情況下如何讓線程實現同步呢?

我們先試試自己寫一個方法讓它實現同步,代碼如下:


    public static  void hello(){
        String name = Thread.currentThread().getName();
        try {
            System.out.println("線程:"+name+" 休眠開始。。。。。。。。。。。。");
            Thread.sleep(1000);
            System.out.println("線程:"+name+" 休眠結束。。。。。。。。。。。。");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    public static  void main(String[] args)  {
        // 計數器初始化爲2
        AtomicInteger count = new AtomicInteger(2);
        executor.execute(() ->{
            hello();
            count.decrementAndGet();
        });
        executor.execute(() ->{
            hello();
            count.decrementAndGet();
        });
        //等待兩個線程執行完畢
        while(count.get() != 0){

        }

        System.out.println("我是在兩個線程執行之後才執行的內容");

    }
count:用於統計線程執行的數量,線程執行-1;AtomicInteger:原子類,可以在多線程中保證共享變量的安全。
decrementAndGet:自減並返回自減以後的結果(原子操作)。
while:線程同步的重點:這裏主要是讓主線程處於循環狀態,直到count被減爲0,也就意味着兩個子線程都已執行完畢。

但是我不推薦這麼做,爲什麼呢?因爲java SDK給我們提供了現成的方法,我們爲啥還要自己去手動實現呢?下面我們就來看看 CountDownLatch是如何實現線程同步:
 

// 創建2個線程的線程池
    private static  Executor executor =   Executors.newFixedThreadPool(2);

    public static  void hello(){
        String name = Thread.currentThread().getName();
        try {
            System.out.println("線程:"+name+" 休眠開始。。。。。。。。。。。。");
            Thread.sleep(1000);
            System.out.println("線程:"+name+" 休眠結束。。。。。。。。。。。。");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    public static  void main(String[] args)  {
        
        //這裏需要注意一點,那就是實例化CountDownLatch的初始大小,一定要和你需要等待線程的數量相同,
        //小了會導致等待的線程提前執行。
        //大了會導致線程一直處於無限循環當中
        CountDownLatch countDownLatch = new CountDownLatch(2);
        executor.execute(() ->{
            hello();
            countDownLatch.countDown();
        });
        executor.execute(() ->{
            hello();
            countDownLatch.countDown();
        });
        //等待兩個線程執行完畢
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("我是在兩個線程執行之後才執行的內容");

    }

爲了效果明顯,我特意在hello方法讓線程休眠1秒。
countDownLatch:實現線程同步的關鍵,實例化一個需要等待的線程數量
countDownLatch.countDown():等待線程數-1。
countDownLatch.await();讓主線程處於等待狀態,直到等待的線程被減爲0(注意:這裏必須要做異常捕獲線程中斷的異常:(InterruptedException);

上面代碼結果如下:

這裏需要注意一點:CountDownLatch的初始大小是不會被重置的,所以使用這個解決方案的時候需要手動重置CountDownLatch線程等待的初始大小。
實現原理:

其實查看源碼,他的實現方式和我之前使用的while類似,他這裏用了for的無限循環,直到等待的線程被減爲0;

那有沒有不需要重新設置線程等待的工具類呢?肯定是有的,那就是接下來要說的:CyclicBarrier

CyclicBarrier

主要通過線程回調來實現線程等待,這裏的實現方式稍微做了一下修改:

// 創建3個線程的線程池,其中一個線程用於回調處理主線程的事情
    private static  Executor executor =   Executors.newFixedThreadPool(3);

    public static  void hello(){
        String name = Thread.currentThread().getName();
        try {
            System.out.println("線程:"+name+" 休眠開始。。。。。。。。。。。。");
            Thread.sleep(1000);
            System.out.println("線程:"+name+" 休眠結束。。。。。。。。。。。。");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    public static  void main(String[] args)  {

        //這裏需要注意一點,那就是實例化CountDownLatch的初始大小,一定要和你需要等待線程的數量相同,
        //小了會導致等待的線程提前執行。
        //大了會導致線程一直處於無限循環當中
        final CyclicBarrier barrier = new CyclicBarrier(2, ()->{ executor.execute(()->printAfter()); });
        executor.execute(() ->{
            hello();
            try {
                barrier.await();
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
        executor.execute(() ->{
            hello();
            try {
                barrier.await();
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
    }

    /**
     * 兩個線程執行完畢之後執行此方法
     */
    private static void printAfter() {

        System.out.println("我是在兩個線程執行之後才執行的內容");
    }

這裏需要注意一點,那就是主函數的輸出語句已經不放在mian方法中了,而是寫在了barrier的回調方法中。當等待的線程執行完畢之後CyclicBarrier的等待線程數會被重置。

CyclicBarrier與CountDownLatch區別
CountDownLatch:解決一個線程等待多個線程場景。
CyclicBarrier:解決一組線程之間的等待場景。
CyclicBarrier支持重置功能,CountDownLatch不支持,這點需要特別注意。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章