常見鎖的使用場景

Semaphore(信號量)-允許多個線程同時訪問(來自javaguide)

synchronized 和 ReentrantLock 都是一次只允許一個線程訪問某個資源,Semaphore(信號量)可以指定多個線程同時訪問某個資源。示例代碼如下:

/**
 * 
 * @author Snailclimb
 * @date 2018年9月30日
 * @Description: 需要一次性拿一個許可的情況
 */
public class SemaphoreExample1 {
    // 請求的數量
    private static final int threadCount = 550;

    public static void main(String[] args) throws InterruptedException {
        // 創建一個具有固定線程數量的線程池對象(如果這裏線程池的線程數量給太少的話你會發現執行的很慢)
        ExecutorService threadPool = Executors.newFixedThreadPool(300);
        // 一次只能允許執行的線程數量。
        final Semaphore semaphore = new Semaphore(20);

        for (int i = 0; i < threadCount; i++) {
            final int threadnum = i;
            threadPool.execute(() -> {// Lambda 表達式的運用
                try {
                    semaphore.acquire();// 獲取一個許可,所以可運行線程數量爲20/1=20
                    test(threadnum);
                    semaphore.release();// 釋放一個許可
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }

            });
        }
        threadPool.shutdown();
        System.out.println("finish");
    }

    public static void test(int threadnum) throws InterruptedException {
        Thread.sleep(1000);// 模擬請求的耗時操作
        System.out.println("threadnum:" + threadnum);
        Thread.sleep(1000);// 模擬請求的耗時操作
    }
}

執行 acquire 方法阻塞,直到有一個許可證可以獲得然後拿走一個許可證;每個 release 方法增加一個許可證,這可能會釋放一個阻塞的acquire方法。然而,其實並 沒有實際的許可證這個對象,Semaphore只是維持了一個可獲得許可證的數量。Semaphore經常用於限制獲取某種資源的線程數量。 當然一次也可以一次拿取和釋放多個許可,不過一般沒有必要這樣做:

                    semaphore.acquire(5);// 獲取5個許可,所以可運行線程數量爲20/5=4
                    test(threadnum);
                    semaphore.release(5);// 獲取5個許可,所以可運行線程數量爲20/5=4

除了 acquire方法之外,另一個比較常用的與之對應的方法是tryAcquire方法,該方法如果獲取不到許可就立即返回false。 Semaphore 有兩種模式,公平模式和非公平模式。

  • 公平模式: 調用acquire的順序就是獲取許可證的順序,遵循FIFO;
  • 非公平模式: 搶佔式的。 Semaphore 對應的兩個構造方法如下:
     public Semaphore(int permits) {
          sync = new NonfairSync(permits);
      }
    
      public Semaphore(int permits, boolean fair) {
          sync = fair ? new FairSync(permits) : new NonfairSync(permits);
      }
    

這兩個構造方法,都必須提供許可的數量,第二個構造方法可以指定是公平模式還是非公平模式,默認非公平模式。

CountDownLatch (倒計時器)

CountDownLatch是一個同步工具類,用來協調多個線程之間的同步。這個工具通常用來控制線程等待,它可以讓某一個線程等待直到倒計時結束,再開始執行。

CountDownLatch 的兩種典型用法

  • 某一線程在開始運行前等待n個線程執行完畢。將 CountDownLatch 的計數器初始化爲n :new CountDownLatch(n),每當一個任務線程執行完畢,就將計數器減1 countdownlatch.countDown(),當計數器的值變爲0時,在CountDownLatch上 await() 的線程就會被喚醒。一個典型應用場景就是啓動一個服務時,主線程需要等待 多個組件加載完畢,之後再繼續執行。

  • 實現多個線程開始執行任務的最大並行性。注意是並行性,不是併發,強調的是多個線程在某一時刻同時開始執行。類似於賽跑,將多個線程放到起點,等待發令槍響, 然後同時開跑。做法是初始化一個共享的 CountDownLatch 對象,將其計數器初始化爲 1 :new CountDownLatch(1),多個線程在開始執行任務前首先 coundownlatch.await(),當主線程調用countDown() 時,計數器變爲0,多個線程同時被喚醒。

    CountDownLatch 的使用示例

    ``` /**

  • @author SnailClimb
  • @date 2018年10月1日
  • @Description: CountDownLatch 使用方法示例 */ public class CountDownLatchExample1 { // 請求的數量 private static final int threadCount = 550;

    public static void main(String[] args) throws InterruptedException { // 創建一個具有固定線程數量的線程池對象(如果這裏線程池的線程數量給太少的話你會發現執行的很慢) ExecutorService threadPool = Executors.newFixedThreadPool(300); final CountDownLatch countDownLatch = new CountDownLatch(threadCount); for (int i = 0; i < threadCount; i++) { final int threadnum = i; threadPool.execute(() -> {// Lambda 表達式的運用 try { test(threadnum); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { countDownLatch.countDown();// 表示一個請求已經被完成 }

         });
     }
     countDownLatch.await();
     threadPool.shutdown();
     System.out.println("finish");  }
    

    public static void test(int threadnum) throws InterruptedException { Thread.sleep(1000);// 模擬請求的耗時操作 System.out.println(“threadnum:” + threadnum); Thread.sleep(1000);// 模擬請求的耗時操作 } } ``` 上面的代碼中,我們定義了請求的數量爲550,當這550個請求被處理完成之後,纔會執行System.out.println(“finish”);。

    CountDownLatch 的不足

    CountDownLatch是一次性的,計數器的值只能在構造方法中初始化一次,之後沒有任何機制再次對其設置值,當CountDownLatch使用完畢後,它不能再次被使用。

CyclicBarrier(循環柵欄)

CyclicBarrier 和 CountDownLatch 非常類似,它也可以實現線程間的技術等待,但是它的功能比 CountDownLatch 更加複雜和強大。主要應用場景和 CountDownLatch 類似。 CyclicBarrier 的字面意思是可循環使用(Cyclic)的屏障(Barrier)。它要做的事情是,讓一組線程到達一個屏障(也可以叫同步點)時被阻塞,直到最後一個線程 到達屏障時,屏障纔會開門,所有被屏障攔截的線程纔會繼續幹活。CyclicBarrier默認的構造方法是 CyclicBarrier(int parties), 其參數表示屏障攔截的線程數量,每個線程調用await方法告訴 CyclicBarrier 我已經到達了屏障,然後當前線程被阻塞。

CyclicBarrier 的應用場景

CyclicBarrier 可以用於多線程計算數據,最後合併計算結果的應用場景。比如我們用一個Excel保存了用戶所有銀行流水,每個Sheet保存一個帳戶近一年的每筆銀行 流水,現在需要統計用戶的日均銀行流水,先用多線程處理每個sheet裏的銀行流水,都執行完之後,得到每個sheet的日均銀行流水, 最後,再用barrierAction用這些線程的計算結果,計算出整個Excel的日均銀行流水。

CyclicBarrier 的使用示例

/**
 * 
 * @author Snailclimb
 * @date 2018年10月1日
 * @Description: 測試 CyclicBarrier 類中帶參數的 await() 方法
 */
public class CyclicBarrierExample2 {
    // 請求的數量
    private static final int threadCount = 550;
    // 需要同步的線程數量
    private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5);

    public static void main(String[] args) throws InterruptedException {
        // 創建線程池
        ExecutorService threadPool = Executors.newFixedThreadPool(10);

        for (int i = 0; i < threadCount; i++) {
            final int threadNum = i;
            Thread.sleep(1000);
            threadPool.execute(() -> {
                try {
                    test(threadNum);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            });
        }
        threadPool.shutdown();
    }

    public static void test(int threadnum) throws InterruptedException, BrokenBarrierException {
        System.out.println("threadnum:" + threadnum + "is ready");
        try {
            cyclicBarrier.await(2000, TimeUnit.MILLISECONDS);
        } catch (Exception e) {
            System.out.println("-----CyclicBarrierException------");
        }
        System.out.println("threadnum:" + threadnum + "is finish");
    }

}

運行結果,如下:

threadnum:0is ready
threadnum:1is ready
threadnum:2is ready
threadnum:3is ready
threadnum:4is ready
threadnum:4is finish
threadnum:0is finish
threadnum:1is finish
threadnum:2is finish
threadnum:3is finish
threadnum:5is ready
threadnum:6is ready
threadnum:7is ready
threadnum:8is ready
threadnum:9is ready
threadnum:9is finish
threadnum:5is finish
threadnum:8is finish
threadnum:7is finish
threadnum:6is finish
......

可以看到當線程數量也就是請求數量達到我們定義的 5 個的時候, await方法之後的方法才被執行。

另外,CyclicBarrier還提供一個更高級的構造函數CyclicBarrier(int parties, Runnable barrierAction), 用於在線程到達屏障時,優先執行barrierAction,方便處理更復雜的業務場景。示例代碼如下:

/**
 * 
 * @author SnailClimb
 * @date 2018年10月1日
 * @Description: 新建 CyclicBarrier 的時候指定一個 Runnable
 */
public class CyclicBarrierExample3 {
    // 請求的數量
    private static final int threadCount = 550;
    // 需要同步的線程數量
    private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5, () -> {
        System.out.println("------當線程數達到之後,優先執行------");
    });

    public static void main(String[] args) throws InterruptedException {
        // 創建線程池
        ExecutorService threadPool = Executors.newFixedThreadPool(10);

        for (int i = 0; i < threadCount; i++) {
            final int threadNum = i;
            Thread.sleep(1000);
            threadPool.execute(() -> {
                try {
                    test(threadNum);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            });
        }
        threadPool.shutdown();
    }

    public static void test(int threadnum) throws InterruptedException, BrokenBarrierException {
        System.out.println("threadnum:" + threadnum + "is ready");
        cyclicBarrier.await();
        System.out.println("threadnum:" + threadnum + "is finish");
    }

}

運行結果,如下:

threadnum:0is ready
threadnum:1is ready
threadnum:2is ready
threadnum:3is ready
threadnum:4is ready
------當線程數達到之後,優先執行------
threadnum:4is finish
threadnum:0is finish
threadnum:2is finish
threadnum:1is finish
threadnum:3is finish
threadnum:5is ready
threadnum:6is ready
threadnum:7is ready
threadnum:8is ready
threadnum:9is ready
------當線程數達到之後,優先執行------
threadnum:9is finish
threadnum:5is finish
threadnum:6is finish
threadnum:8is finish
threadnum:7is finish
......

CyclicBarrier和CountDownLatch的區別

CountDownLatch是計數器,只能使用一次,而CyclicBarrier的計數器提供reset功能,可以多次使用。但是我不那麼認爲它們之間的區別僅僅就是這麼簡單的一點。我們來從jdk作者設計的目的來看,javadoc是這麼描述它們的:

CountDownLatch: A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.(CountDownLatch: 一個或者多個線程,等待其他多個線程完成某件事情之後才能執行;) CyclicBarrier : A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point.(CyclicBarrier : 多個線程互相等待,直到到達同一個同步點,再繼續一起執行。)

對於CountDownLatch來說,重點是“一個線程(多個線程)等待”,而其他的N個線程在完成“某件事情”之後,可以終止,也可以等待。而對於CyclicBarrier,重點是多個線程,在任意一個線程沒有完成,所有的線程都必須等待。

CountDownLatch是計數器,線程完成一個記錄一個,只不過計數不是遞增而是遞減,而CyclicBarrier更像是一個閥門,需要所有線程都到達,閥門才能打開,然後繼續執行。 兩者區別

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