java.util.concurrent(J.U.C)大大提高了併發性能,AQS 被認爲是 J.U.C 的核心。
CountdownLatch
用來控制一個線程等待多個線程。
維護了一個計數器 cnt,每次調用 countDown() 方法會讓計數器的值減 1,減到 0 的時候,那些因爲調用 await() 方法而在等待的線程就會被喚醒。
原理就是await時加共享鎖,countDown時釋放共享鎖
public class CountdownLatchExample {
public static void main(String[] args) throws InterruptedException {
final int totalThread = 10;
CountDownLatch countDownLatch = new CountDownLatch(totalThread);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < totalThread; i++) {
executorService.execute(() -> {
System.out.print("run..");
countDownLatch.countDown();
});
}
countDownLatch.await();
System.out.println("end");
executorService.shutdown();
}
}
run..run..run..run..run..run..run..run..run..run..end
CyclicBarrier
用來控制多個線程互相等待,只有當多個線程都到達時,這些線程纔會繼續執行。
和 CountdownLatch 相似,都是通過維護計數器來實現的。線程執行 await() 方法之後計數器會減 1,並進行等待,直到計數器爲 0,所有調用 await() 方法而在等待的線程才能繼續執行。
CyclicBarrier 和 CountdownLatch 的一個區別是,CyclicBarrier 的計數器通過調用 reset() 方法可以循環使用,所以它才叫做循環屏障。
CyclicBarrier 有兩個構造函數,其中 parties 指示計數器的初始值,barrierAction 在所有線程都到達屏障的時候會執行一次。
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
public CyclicBarrier(int parties) {
this(parties, null);
}
public class CyclicBarrierExample {
public static void main(String[] args) {
final int totalThread = 10;
CyclicBarrier cyclicBarrier = new CyclicBarrier(totalThread);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < totalThread; i++) {
executorService.execute(() -> {
System.out.print("before..");
try {
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
System.out.print("after..");
});
}
executorService.shutdown();
}
}
before..before..before..before..before..before..before..before..before..before..after..after..after..after..after..after..after..after..after..after..
Semaphore
Semaphore 類似於操作系統中的信號量,可以控制對互斥資源的訪問線程數。
信號量,控制併發量,分佈式系統也有相似概念,稱爲“限流”。限流是有必要的,比如我們的系統裏有一個導出彙總excel的功能,不限制併發量,系統立馬就OOM了
以下代碼模擬了對某個服務的併發請求,每次只能有 3 個客戶端同時訪問,請求總數爲 10。
public class ThreadPoolTest {
private static AtomicInteger ai = new AtomicInteger();
//我們新建一條三車道的高速公路
private static Semaphore semaphore = new Semaphore(3);
public static void main(String[] args) {
//高速入口堵了20輛車
ExecutorService executors = Executors.newFixedThreadPool(20);
for (int i = 0; i < 20; i++) {
executors.execute(new Runnable() {
@Override
public void run() {
try {
//放行一輛
semaphore.acquire();
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
} finally {
//車道空出來了
semaphore.release();
}
System.out.println(ai.getAndIncrement());
}
});
}
}
}
FutureTask
在介紹 Callable 時我們知道它可以有返回值,返回值通過 Future 進行封裝。FutureTask 實現了 RunnableFuture 接口,該接口繼承自 Runnable 和 Future 接口,這使得 FutureTask 既可以當做一個任務執行,也可以有返回值。
public class FutureTask<V> implements RunnableFuture<V>
public interface RunnableFuture<V> extends Runnable, Future<V>
FutureTask 可用於異步獲取執行結果或取消執行任務的場景。當一個計算任務需要執行很長時間,那麼就可以用 FutureTask 來封裝這個任務,主線程在完成自己的任務之後再去獲取結果。
public class FutureTaskExample {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> futureTask = new FutureTask<Integer>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int result = 0;
for (int i = 0; i < 100; i++) {
Thread.sleep(10);
result += i;
}
return result;
}
});
Thread computeThread = new Thread(futureTask);
computeThread.start();
Thread otherThread = new Thread(() -> {
System.out.println("other task is running...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
otherThread.start();
System.out.println(futureTask.get());
}
}
other task is running...
4950
BlockingQueue
java.util.concurrent.BlockingQueue 接口有以下阻塞隊列的實現:
- FIFO 隊列 :LinkedBlockingQueue、ArrayBlockingQueue(固定長度)
- 優先級隊列 :PriorityBlockingQueue
提供了阻塞的 take() 和 put() 方法:如果隊列爲空 take() 將阻塞,直到隊列中有內容;如果隊列爲滿 put() 將阻塞,直到隊列有空閒位置。
使用 BlockingQueue 實現生產者消費者問題
public class ProducerConsumer {
private static BlockingQueue<String> queue = new ArrayBlockingQueue<>(5);
private static class Producer extends Thread {
@Override
public void run() {
try {
queue.put("product");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print("produce..");
}
}
private static class Consumer extends Thread {
@Override
public void run() {
try {
String product = queue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print("consume..");
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
Producer producer = new Producer();
producer.start();
}
for (int i = 0; i < 5; i++) {
Consumer consumer = new Consumer();
consumer.start();
}
for (int i = 0; i < 3; i++) {
Producer producer = new Producer();
producer.start();
}
}
produce..produce..consume..consume..produce..consume..produce..consume..produce..consume..
ForkJoin
主要用於並行計算中,和 MapReduce 原理類似,都是把大的計算任務拆分成多個小任務並行計算。
public class ForkJoinExample extends RecursiveTask<Integer> {
private final int threshold = 5;
private int first;
private int last;
public ForkJoinExample(int first, int last) {
this.first = first;
this.last = last;
}
@Override
protected Integer compute() {
int result = 0;
if (last - first <= threshold) {
// 任務足夠小則直接計算
for (int i = first; i <= last; i++) {
result += i;
}
} else {
// 拆分成小任務
int middle = first + (last - first) / 2;
ForkJoinExample leftTask = new ForkJoinExample(first, middle);
ForkJoinExample rightTask = new ForkJoinExample(middle + 1, last);
leftTask.fork();
rightTask.fork();
result = leftTask.join() + rightTask.join();
}
return result;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
ForkJoinExample example = new ForkJoinExample(1, 10000);
ForkJoinPool forkJoinPool = new ForkJoinPool();
Future result = forkJoinPool.submit(example);
System.out.println(result.get());
}
ForkJoin 使用 ForkJoinPool 來啓動,它是一個特殊的線程池,線程數量取決於 CPU 核數。
public class ForkJoinPool extends AbstractExecutorService
ForkJoinPool 實現了工作竊取算法來提高 CPU 的利用率。每個線程都維護了一個雙端隊列,用來存儲需要執行的任務。工作竊取算法允許空閒的線程從其它線程的雙端隊列中竊取一個任務來執行。竊取的任務必須是最晚的任務,避免和隊列所屬線程發生競爭。例如下圖中,Thread2 從 Thread1 的隊列中拿出最晚的 Task1 任務,Thread1 會拿出 Task2 來執行,這樣就避免發生競爭。但是如果隊列中只有一個任務時還是會發生競爭。