- Posted by 微博@Yangsc_o
- 原創文章,版權聲明:自由轉載-非商用-非衍生-保持署名 | Creative Commons BY-NC-ND 3.0
摘要
本文主要簡單介紹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 的方法不是很多,將它們一個個列舉出來:
- await() throws InterruptedException:調用該方法的線程等到構造方法傳入的 N 減到 0 的時候,才能繼續往下執行;
- await(long timeout, TimeUnit unit):與上面的 await 方法功能一致,只不過這裏有了時間限制,調用該方法的線程等到指定的 timeout 時間後,不管 N 是否減至爲 0,都會繼續往下執行;
- countDown():使 CountDownLatch 初始值 N 減 1;
- 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 類中比較重要的幾個方法:
- public void acquire(): 用來獲取一個許可,若無許可能夠獲得,則會一直等待,直到獲得許
可。 - public void acquire(int permits):獲取 permits 個許可
- public void release() { } :釋放許可。注意,在釋放許可之前,必須先獲獲得許可。
- public void release(int permits) { }:釋放 permits 個許可
上面 4 個方法都會被阻塞,如果想立即得到執行結果,可以使用下面幾個方法13/04/2018 Page 86 of 283 - public boolean tryAcquire():嘗試獲取一個許可,若獲取成功,則立即返回 true,若獲取失
敗,則立即返回 false - public boolean tryAcquire(long timeout, TimeUnit unit):嘗試獲取一個許可,若在指定的
時間內獲取成功,則立即返回 true,否則則立即返回 false - public boolean tryAcquire(int permits):嘗試獲取 permits 個許可,若獲取成功,則立即返
回 true,若獲取失敗,則立即返回 false - public boolean tryAcquire(int permits, long timeout, TimeUnit unit): 嘗試獲取 permits
個許可,若在指定的時間內獲取成功,則立即返回 true,否則則立即返回 false - 還可以通過 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