溫故知新-多線程-forkjoin、CountDownLatch、CyclicBarrier、Semaphore用法



摘要

本文主要簡單介紹forkjoin、CountDownLatch、CyclicBarrier、Semaphore的常見用法;

forkjoin

從JDK1.7開始,Java提供Fork/Join框架用於並行執行任務,它的思想就是講一個大任務分割成若干小任務,最終彙總每個小任務的結果得到這個大任務的結果。

這種思想和MapReduce很像(input --> split --> map --> reduce --> output)

主要有兩步:

  • 第一、任務切分;
  • 第二、結果合併

它的模型大致是這樣的:線程池中的每個線程都有自己的工作隊列(PS:這一點和ThreadPoolExecutor不同,ThreadPoolExecutor是所有線程公用一個工作隊列,所有線程都從這個工作隊列中取任務),當自己隊列中的任務都完成以後,會從其它線程的工作隊列中偷一個任務執行,這樣可以充分利用資源。

假如我們需要做一個比較大的任務,我們可以把這個任務分割爲若干互不依賴的子任務,爲了減少線程間的競爭,於是把這些子任務分別放到不同的隊列裏,併爲每個隊列創建一個單獨的線程來執行隊列裏的任務,線程和隊列一一對應,比如A線程負責處理A隊列裏的任務。但是有的線程會先把自己隊列裏的任務幹完,而其他線程對應的隊列裏還有任務等待處理。幹完活的線程與其等着,不如去幫其他線程幹活,於是它就去其他線程的隊列裏竊取一個任務來執行。而在這時它們會訪問同一個隊列,所以爲了減少竊取任務線程和被竊取任務線程之間的競爭,通常會使用雙端隊列,被竊取任務線程永遠從雙端隊列的頭部拿任務執行,而竊取任務的線程永遠從雙端隊列的尾部拿任務執行。

工作竊取算法的優點是充分利用線程進行並行計算,並減少了線程間的競爭,其缺點是在某些情況下還是存在競爭,比如雙端隊列裏只有一個任務時。並且消耗了更多的系統資源,比如創建多個線程和多個雙端隊列。

public class ForkJoinDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        long start = System.currentTimeMillis();
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        ForkJoinTask<Long> task = new MyForkJoinTask(0L, 10_0000_0000L);
        ForkJoinTask<Long> submit = forkJoinPool.submit(task);
        Long sum = submit.get();
        long end = System.currentTimeMillis();
        System.out.println("sum=" + sum + " 時間:" + (end - start));
    }


}

class MyForkJoinTask extends RecursiveTask<Long> {

    private Long start;
    private Long end;
    // 臨界值
    private Long temp = 10000L;

    public MyForkJoinTask(Long start, Long end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        if ((end - start) < temp) {
            Long sum = 0L;
            for (Long i = start; i <= end; i++) {
                sum += i;
            }
            return sum;
        } else { // forkjoin 遞歸
            long middle = (start + end) / 2; 
            MyForkJoinTask task1 = new MyForkJoinTask(start, middle);
            task1.fork(); 
            MyForkJoinTask task2 = new MyForkJoinTask(middle + 1, end);
            task2.fork(); 
            return task1.join() + task2.join();
        }
    }
}

CountDownLatch

CountDownLatch 的方法不是很多,將它們一個個列舉出來:

  1. await() throws InterruptedException:調用該方法的線程等到構造方法傳入的 N 減到 0 的時候,才能繼續往下執行;
  2. await(long timeout, TimeUnit unit):與上面的 await 方法功能一致,只不過這裏有了時間限制,調用該方法的線程等到指定的 timeout 時間後,不管 N 是否減至爲 0,都會繼續往下執行;
  3. countDown():使 CountDownLatch 初始值 N 減 1;
  4. long getCount():獲取當前 CountDownLatch 維護的值
public class CountDownLatchDemo {
    private static CountDownLatch startSignal = new CountDownLatch(1);
    //用來表示裁判員需要維護的是6個運動員
    private static CountDownLatch endSignal = new CountDownLatch(6);

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(6);
        for (int i = 0; i < 6; i++) {
            executorService.execute(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + " 運動員等待裁判員響哨!!!");
                    startSignal.await();
                    System.out.println(Thread.currentThread().getName() + "正在全力衝刺");
                    endSignal.countDown();// 數量-1
                    System.out.println(Thread.currentThread().getName() + "  到達終點");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        System.out.println("裁判員發號施令啦!!!");
        startSignal.countDown(); // 數量-1
        endSignal.await();
        System.out.println("所有運動員到達終點,比賽結束!");
        executorService.shutdown();
    }

    @SneakyThrows
    public static void test0() {
        // 總數是6,必須要執行任務的時候,再使用!
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + " Go out");
                countDownLatch.countDown();
            }, String.valueOf(i)).start();
        }
        countDownLatch.await(); // 等待計數器歸零,然後再向下執行
        System.out.println("Close Door");

    }
}

CyclicBarrier

當多個線程都達到了指定點後,才能繼續往下繼續執行。這就有點像報數的感覺,假設 6 個線程就相當於 6 個運動員,到賽道起點時會報數進行統計,如果剛好是 6 的話,這一波就湊齊了,才能往下執行。**CyclicBarrier 在使用一次後,下面依然有效,可以繼續當做計數器使用,這是與 CountDownLatch 的區別之一。**這裏的 6 個線程,也就是計數器的初始值 6,是通過 CyclicBarrier 的構造方法傳入的。

下面來看下 CyclicBarrier 的主要方法:

// 等到所有的線程都到達指定的臨界點 await() throws InterruptedException, BrokenBarrierException

// 與上面的await方法功能基本一致,只不過這裏有超時限制,阻塞等待直至到達超時時間爲止 await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException

//獲取當前有多少個線程阻塞等待在臨界點上 int getNumberWaiting()

//用於查詢阻塞等待的線程是否被中斷 boolean isBroken()

public class CyclicBarrierDemo {


    public static void main(String[] args) {
        test();
    }
    public static void test() {

        CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()-> {
            System.out.println("召喚神龍成功!");
        });
        for (int i = 1; i <=7 ; i++) {
            final int temp = i;
            // lambda能操作到 i 嗎
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"收集"+temp+"個龍珠");
                try {
                    cyclicBarrier.await(); // 等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

Semaphore

Semaphore(信號量)是用來控制同時訪問特定資源的線程數量,它通過協調各個線程,以保證合理的使用公共資源。很多年以來,我都覺得從字面上很難理解Semaphore所表達的含義,只能把它比作是控制流量的紅綠燈,比如XX馬路要限制流量,只允許同時有一百輛車在這條路上行使,其他的都必須在路口等待,所以前一百輛車會看到綠燈,可以開進這條馬路,後面的車會看到紅燈,不能駛入XX馬路,但是如果前一百輛中有五輛車已經離開了XX馬路,那麼後面就允許有5輛車駛入馬路,這個例子裏說的車就是線程,駛入馬路就表示線程在執行,離開馬路就表示線程執行完成,看見紅燈就表示線程被阻塞,不能執行。

Semaphore 類中比較重要的幾個方法:

  1. public void acquire(): 用來獲取一個許可,若無許可能夠獲得,則會一直等待,直到獲得許
    可。
  2. public void acquire(int permits):獲取 permits 個許可
  3. public void release() { } :釋放許可。注意,在釋放許可之前,必須先獲獲得許可。
  4. public void release(int permits) { }:釋放 permits 個許可
    上面 4 個方法都會被阻塞,如果想立即得到執行結果,可以使用下面幾個方法13/04/2018 Page 86 of 283
  5. public boolean tryAcquire():嘗試獲取一個許可,若獲取成功,則立即返回 true,若獲取失
    敗,則立即返回 false
  6. public boolean tryAcquire(long timeout, TimeUnit unit):嘗試獲取一個許可,若在指定的
    時間內獲取成功,則立即返回 true,否則則立即返回 false
  7. public boolean tryAcquire(int permits):嘗試獲取 permits 個許可,若獲取成功,則立即返
    回 true,若獲取失敗,則立即返回 false
  8. public boolean tryAcquire(int permits, long timeout, TimeUnit unit): 嘗試獲取 permits
    個許可,若在指定的時間內獲取成功,則立即返回 true,否則則立即返回 false
  9. 還可以通過 availablePermits()方法得到可用的許可數目。

應用場景

Semaphore可以用於做流量控制,特別公用資源有限的應用場景;

public class SemaphoreDemo {
    public static void main(String[] args) {
        // 線程數量:停車位! 限流!
        Semaphore semaphore = new Semaphore(3);
        for (int i = 1; i <=6 ; i++) {
            new Thread(()->{
                try {
                 		 // acquire() 得到
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName()+"搶到車位");
                            TimeUnit.SECONDS.sleep(2);
                    System.out.println(Thread.currentThread().getName()+"離開車位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release(); // release() 釋放
                }
            },String.valueOf(i)).start();
        }
    }
}

預告:下一篇會分析一下AQS的實現原理,因爲CountDownLatch、CyclicBarrier、Semaphore都是基於AQS實現的;

參考

JDK 7 中的 Fork/Join 模式
一文秒懂 Java Fork/Join
併發工具類(三)控制併發線程數的Semaphore


你的鼓勵也是我創作的動力

打賞地址

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