本部分介紹幾種在高并發場景中常用的設計模式:線程安全的單例模式、ForkJoin模式、生產者——消費者模式、Master-Worker模式和Future模式。
線程安全的單例模式
單例模式很常見,一般用于全局對象管理,比如XML讀寫實例、系統配置實例、任務調度實例、數據庫連接池實例等。
從餓漢式單例到懶漢式單例
按照單例對象初始化的時機,單例模式分為懶漢式和餓漢式,懶漢式單例對象在類被加載時就直接被初始化。
public class Singleton1 {
// 簡單的懶漢單例模式
private Singleton1() {} // 私有構造器
private static final Singleton1 single = new Singleton1();
public static Singleton1 getInstance() {
return single;
}
}
餓漢式單例模式的優點是足夠簡單、安全,缺點是單例對象在類被加載時實例就創建,很多時候類被加載時并不需要初始化單例。
懶漢式單例在使用的時候才進行初始化。
public class ASingleton {
static ASingleton instance; // 靜態成員
// 私有構造器
private ASingleton() {}
static ASingleton getInstance() {
if(instance == null) {
instance = new ASingleton();
}
return instance;
}
}
這兩種單例模式的問題是都不是線程安全的。
使用內置鎖保護懶漢式單例
可以為getInstance()方法添加synchronized內置鎖進行單利獲取同步。
public class ASingleton {
static ASingleton instance; // 靜態成員
// 私有構造器
private ASingleton() {}
static synchronized ASingleton getInstance() {
if(instance == null) {
instance = new ASingleton();
}
return instance;
}
}
使用synchronized后每次調用都要同步,爭用激烈的場景下,內置鎖會升級為重量級鎖,開銷大,性能差,所以高并發下不推薦使用。
雙重檢查鎖單例模式
實際上,單例模式的加鎖操作只有單例在第一次創建的時候才用到,之后的單例獲取操作沒必要再加鎖。所以,可以先判斷單例對象是否已經被初始化,如果沒有,加鎖后再初始化,這種模式叫做雙重檢查鎖(Double Checked Locking)單例模式。
public class ASingleton {
static ASingleton instance; // 靜態成員
// 私有構造器
private ASingleton() {}
static synchronized ASingleton getInstance() {
if(instance == null) { // 檢查1
synchronized (ASingleton.class) {
if(instance == null) { // 檢查2
instance = new ASingleton();
}
}
}
return instance;
}
}
雙重檢查鎖單例模式主要包括三步:
- 檢查對象是否被初始化,如果已經被初始化,就立即返回單例對象,此次檢查無需鎖進行同步。
- 如果單例沒有被初始化,就試圖進入臨界區進行初始化操作,此時采取獲取鎖。
- 進入臨界區后,再次檢查單例對象是否已經被初始化,如果沒有就初始化一個實例。
需要二次檢查的原因是在多線程場景下有可能多個線程通過了第一次檢查。
雙重檢查不僅避免了單例對象返回初始化,而且除了初始化的時候需要現加鎖外,后續的所有調用都不需要加鎖而直接返回單例,從而提升了獲取單例時的性能。
使用雙重檢查鎖+volatile
上述的雙重檢查鎖單例模式還有問題,看一下初始化代碼:
instance = new Singleton();
這行代碼轉換成匯編指令后,大致會細分成三個:
- 分配一塊內存M。
- 在內存M上初始化Singleton對象。
- M的地址賦值給instance變量。
編譯器、CPU都可能對沒有內存屏障、數據依賴關系的操作進行重排序,上述的三個指令優化后可能變成了:
- 分配一塊內存M。
- M的地址賦值給instance變量。
- 在內存M上初始化Singleton對象。
這樣有可能導致isntance未初始化就被其他線程調用,得到一個未初始化的對象。
因此需要通過volatile禁止指令重排。
public class ASingleton {
static volatile ASingleton instance; // 靜態成員
// 私有構造器
private ASingleton() {}
static synchronized ASingleton getInstance() {
if(instance == null) { // 檢查1
synchronized (ASingleton.class) {
if(instance == null) { // 檢查2
instance = new ASingleton();
}
}
}
return instance;
}
}
使用靜態內部類實現懶漢式單例模式
雙重檢查鎖+volatile的方式能實現高性能、線程安全的單例模式,但是該實現的底層原理比較復雜,寫法繁瑣。另一種簡單的單例模式實現是使用靜態內部類實例懶漢式單例模式。
public class Singleton {
// 靜態內部類
private static class LazyHolder {
// 通過final保證初始化時的線程安全
private static final Singleton INSTANCE = new Singleton();
}
// 私有構造器
private Singleton() {}
// 獲取單例的方法
public static final Singleton getInstance() {
// 返回內部類的靜態、最終成員
return LazyHolder.INSTANCE;
}
}
Master-Worker模式
Master-Worker模式是一種常見的高并發模式,核心思想是分而治之,將任務調度和執行分離,調度任務的角色為Master,Master負責接收、分配任務和合并任務結果,Worker負責執行任務。
Master-Worker模式的參考實現
Master-Worker模式理解起來不難,但實現起來有很多要點需要注意。下面看一個簡單的累加求和的任務使用Master-Worker模式實現。
Master的參考代碼
public class Master<T extends Task, R> {
// 所有Worker的集合
private HashMap<String, Worker<T, R>> workers = new HashMap<>();
// 任務的集合
private LinkedBlockingDeque<T> taskQueue = new LinkedBlockingDeque<>();
// 任務c處理結果集合
private Map<String, R> resultMap = new ConcurrentHashMap<>();
// Master的任務調度線程
private Thread thread = null;
private AtomicLong sum = new AtomicLong(0);
public Master(int workerCount) {
// 每個Worker對象都需要持有queue的引用,用于領任務與提交結果
for(int i = 0; i < workerCount; i++) {
Worker<T, R> worker = new Worker<>();
workers.put("子節點:" + i, worker);
}
thread = new Thread(() -> this.execute());
thread.start();
}
// 提交任務
public void submit(T task) {
taskQueue.add(task);
}
private void resultCallBack(Object o) {
Task<R> task = (Task<R>)o;
String taskName = "Worker:" + task.getWorkerId()
+ "-" + "Task:" + task.getId();
R result = task.getResult();
resultMap.put(taskName, result);
sum.getAndAdd((Integer)result); // 和的累加
}
// 啟動所有的子任務
private void execute() {
for(;;) {
for(Map.Entry<String, Worker<T, R>> entry : workers.entrySet()) {
T task = null;
try {
task = this.taskQueue.take(); // 獲取任務
Worker worker = entry.getValue(); // 獲取節點
worker.submit(task, this::resultCallBack); // 分配任務
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 獲取最終給結果
public void printResult() {
System.out.println("---------sum is : " + sum.get());
for(Map.Entry<String, R> entry : resultMap.entrySet()) {
String taskName = entry.getKey();
System.out.println(taskName + ":" + entry.getValue());
}
}
}
Master負責接收客戶端提交的任務,然后通過阻塞隊列對任務進行緩存。Master所擁有的線程作為阻塞隊列的消費者,不斷從阻塞隊列獲取任務并輪流分給Worker。
Worker的參考代碼
public class Worker<T extends Task, R> {
// 接受任務的阻塞隊列
private LinkedBlockingDeque<T> taskQueue = new LinkedBlockingDeque<>();
// Worker 的編號
static AtomicInteger index = new AtomicInteger(1);
private int workerId;
private Thread thread = null;
public Worker() {
this.workerId = index.getAndIncrement();
thread = new Thread(() -> this.run());
thread.start();
}
// 輪詢執行任務
public void run() {
// 輪詢啟動所有的子任務
for(;;) {
try {
// 從阻塞隊列中提取任務
T task = this.taskQueue.take();
task.setWorkerId(workerId);
task.execute();
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}
// 接收任務列異步隊列
public void submit(T task, Consumer<R> action) {
task.resultAction = action; // 設置回調
try {
this.taskQueue.put(task);
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}
Worker接收Master分配的任務,同樣也通過阻塞隊列對局部任務進行緩存。Worker所擁有的線程作為局部任務的阻塞隊列的消費者,不斷從阻塞隊列獲取任務并執行,執行完成后回調Master傳遞過來的回調函數。
異步任務類
public class Task<R> {
static AtomicInteger index = new AtomicInteger(1);
// 任務的回調函數
public Consumer<Task<R>> resultAction;
// 任務的id
private int id;
private int workerId;
// 計算結果
R result = null;
public Task() {
this.id = index.getAndIncrement();
}
public void execute() {
this.result = this.doExecute();
// 執行回調函數
resultAction.accept(this);
}
// 由子類實現
protected R doExecute() {
return null;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getWorkerId() {
return workerId;
}
public void setWorkerId(int workerId) {
this.workerId = workerId;
}
public R getResult() {
return result;
}
}
異步任務類在執行子任務的 doExecute 方法后,回調一下Master傳遞過來的回調函數,將執行完成后的任務進行回調。
測試用例
下面測試一下。
public class MasterWorkerTest {
// 簡單任務
static class SimpleTask extends Task<Integer> {
@Override
protected Integer doExecute() {
System.out.println("task " + getId() + " is done ");
return getId();
}
}
@Test
public void testMasterWorker() {
// 創建master,包含4個Worker,并啟動Master的執行線程
Master<SimpleTask, Integer> master = new Master<>(4);
ScheduledExecutorService pool1 = Executors.newScheduledThreadPool(1);
pool1.scheduleAtFixedRate(() -> master.submit(
new SimpleTask()
), 0,1, TimeUnit.SECONDS);
ScheduledExecutorService pool2 = Executors.newScheduledThreadPool(1);
pool2.scheduleAtFixedRate(() -> master.printResult(),0, 2, TimeUnit.SECONDS);
try {
Thread.sleep(10000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
執行測試用例,結果如下:
---------sum is : 0
task 1 is done
task 2 is done
task 3 is done
---------sum is : 6
Worker:1-Task:3:3
Worker:3-Task:1:1
Worker:2-Task:2:2
task 4 is done
task 5 is done
---------sum is : 15
Worker:4-Task:4:4
Worker:3-Task:5:5
Worker:1-Task:3:3
Worker:3-Task:1:1
Worker:2-Task:2:2
task 6 is done
task 7 is done
---------sum is : 28
Worker:1-Task:7:7
Worker:2-Task:6:6
Worker:4-Task:4:4
Worker:3-Task:5:5
Worker:1-Task:3:3
Worker:3-Task:1:1
Worker:2-Task:2:2
task 8 is done
task 9 is done
---------sum is : 36
Worker:1-Task:7:7
Worker:2-Task:6:6
Worker:4-Task:8:8
Worker:3-Task:9:9
Worker:4-Task:4:4
Worker:3-Task:5:5
Worker:1-Task:3:3
Worker:3-Task:1:1
Worker:2-Task:2:2
task 10 is done
task 11 is done
---------sum is : 66
Worker:1-Task:7:7
Worker:2-Task:6:6
Worker:4-Task:8:8
Worker:2-Task:10:10
Worker:3-Task:9:9
Worker:4-Task:4:4
Worker:1-Task:11:11
Worker:3-Task:5:5
Worker:1-Task:3:3
Worker:3-Task:1:1
Worker:2-Task:2:2
Process finished with exit code 0
Netty 中 Master-Worker 模式的實現
高性能傳輸模式 Reactor 模式就是 Master-Worker 模式在傳輸領域的一種應用。基于Java的NIO技術,Netty設計了一套優秀的、高性能Reactor(反應器)模式的具體實現。
在Netty中,EventLoop反應器內部都有一個線程負責Java NIO選擇器的事件輪詢,然后進行對應的事件分發,事件分發的目標就是Netty的Handler處理程序(含用戶定義的業務處理邏輯)。
Netty服務器程序中需要設置兩個EventLoopGroup輪詢組,一個組負責連接的監聽和接收,一個組負責IO傳輸事件和輪詢與分發,兩個輪詢組的職責具體如下:
負責新連接的監聽和接收的EventLoopGroup輪詢組中的反應器完成查詢通道的新連接IO事件查詢,這些反應器有點像負責招工的包工頭,因此被稱為BOSS。
另一個輪詢組中的反應器完成查詢所有子通道的IO事件,并且執行對應的Handler處理程序完成IO處理,例如數據的輸入和輸出,這個輪詢組被稱為Worker。
Nginx 中Master-Worker模式的實現
Nginx 在啟動后會以daemon方式在后臺運行,它的后臺進程有兩類:一類稱為Master進程(管理進程),另一個類稱為Worker進程(工作進程)。
Nginx 的 Master 進程主要負責調度 Worker 進程,比如加載配置,啟動工作進程,接收來自外界的信號,向各Worker進程發送信號,監控Worker進程的運行狀態等。Master進程負責創建監聽套接口,交由Worker進程進行連接監聽。
Worker進程主要用來處理網絡時間,當一個Worker進程在介紹一條連接通道之后,就開始讀取請求,解析請求,處理請求,處理完成產生的數據后,再返回給客戶端,最后斷開連接通道。
ForkJoin模式
ForkJoin模式也是“分而治之”思想的一種應用,不過它沒有Master角色,ForkJoin模式將大的任務分割成小的任務,一直到任務的規模足夠小,可以使用很簡單、直接的方式來完成。
ForkJoin模式的原理
ForkJoin模式先把一個大任務分解成許多個獨立的子任務,然后開啟多個線程并行區處理這些子任務。有可能子任務還是很大而且需要進一步分解,最終得到足夠小的任務。
ForkJoin框架
JUC包提供了一套ForkJoin框架的實現,具體以ForkJoinPool線程池的形式提供,并且該線程池在 Java 8 的 Lambda 并行流框架中充當著底層框架的角色。ForkJoin框架包含如下組件:
ForkJoinPool:執行任務的線程池,繼承了AbstractExectutorService 類。
ForkJoinWorkerThread:執行任務的工作線程。每個線程都維護著一個內部隊列,用于存放“內部任務”,該類繼承了Thread類。
ForkJoinTask:用于ForkJoinPool的任務抽象類,實現了Future接口。
RecursiveTask:帶返回結果的遞歸執行任務,是ForkJoinTask的子類,在子任務帶返回結果時使用。
RecursiveAction:不返回結果的遞歸執行任務,是ForkJoinTask的子類,在子任務不帶返回結果時使用
日常使用一般通過繼承 RecursiveTask 或 RecursiveAction 來實現自定義的任務類,自定義任務類需要實現這些子類的 compute 方法,該方法的執行流程一般如下:
if 任務足夠小
直接返回結果
else
分割成N個任務
依次調用每個子任務的fork方法執行子任務
依次調用每個子任務的join方法,等待子任務完成,然后合并執行結果。
ForkJoin框架使用實戰
可遞歸執行的異步任務類AccumulateTask
public class AccmulateTask extends RecursiveTask<Integer> {
private static final int THRESHOLD = 2;
// 累加的起始編號
private int start;
// 累加的結束編號
private int end;
public AccmulateTask(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
int sum = 0;
// 判斷任務的規模,若規模小則可以直接計算
boolean canCompute = (end - start) <= THRESHOLD;
// 如果任務已經足夠小,則可以直接計算
if(canCompute) {
//直接結算并返回結果,Recursive結束
for(int i = start; i <= end; i++) {
sum += i;
}
System.out.println("執行任務,計算" + start + "到" +
end + "的和,結果是:" + sum);
} else {
System.out.println("切割任務:將" + start + "到" + end + "的和一分為二");
int middle = (start + end) / 2;
// 切割成兩個子任務
AccmulateTask lTask = new AccmulateTask(start, middle);
AccmulateTask rTask = new AccmulateTask(middle + 1, end);
// 依次調用每個子任務的fork方法執行子任務
lTask.fork();
rTask.fork();
// 等待子任務完成,依次調用每個子任務的join方法合并執行結果
int leftResult = lTask.join();
int rightResult = rTask.join();
// 合并子任務執行結果
sum = leftResult + rightResult;
}
return sum;
}
}
使用ForkJoinPool調度AccumulateTask
使用ForkJoinPool調度AccumulateTask的示例代碼如下:
public class AccmulateTask extends RecursiveTask<Integer> {
private static final int THRESHOLD = 2;
// 累加的起始編號
private int start;
// 累加的結束編號
private int end;
public AccmulateTask(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
int sum = 0;
// 判斷任務的規模,若規模小則可以直接計算
boolean canCompute = (end - start) <= THRESHOLD;
// 如果任務已經足夠小,則可以直接計算
if(canCompute) {
//直接結算并返回結果,Recursive結束
for(int i = start; i <= end; i++) {
sum += i;
}
System.out.println("執行任務,計算" + start + "到" +
end + "的和,結果是:" + sum);
} else {
System.out.println("切割任務:將" + start + "到" + end + "的和一分為二");
int middle = (start + end) / 2;
// 切割成兩個子任務
AccmulateTask lTask = new AccmulateTask(start, middle);
AccmulateTask rTask = new AccmulateTask(middle + 1, end);
// 依次調用每個子任務的fork方法執行子任務
lTask.fork();
rTask.fork();
// 等待子任務完成,依次調用每個子任務的join方法合并執行結果
int leftResult = lTask.join();
int rightResult = rTask.join();
// 合并子任務執行結果
sum = leftResult + rightResult;
}
return sum;
}
}
執行測試用例,運行結果如下:
切割任務:將1到100的和一分為二
切割任務:將1到50的和一分為二
切割任務:將1到25的和一分為二
切割任務:將1到13的和一分為二
切割任務:將1到7的和一分為二
切割任務:將1到4的和一分為二
執行任務,計算1到2的和,結果是:3
執行任務,計算3到4的和,結果是:7
執行任務,計算5到7的和,結果是:18
切割任務:將8到13的和一分為二
執行任務,計算8到10的和,結果是:27
執行任務,計算11到13的和,結果是:36
切割任務:將14到25的和一分為二
切割任務:將14到19的和一分為二
執行任務,計算14到16的和,結果是:45
執行任務,計算17到19的和,結果是:54
切割任務:將20到25的和一分為二
執行任務,計算20到22的和,結果是:63
執行任務,計算23到25的和,結果是:72
切割任務:將26到50的和一分為二
切割任務:將26到38的和一分為二
切割任務:將26到32的和一分為二
切割任務:將26到29的和一分為二
執行任務,計算26到27的和,結果是:53
執行任務,計算28到29的和,結果是:57
執行任務,計算30到32的和,結果是:93
切割任務:將33到38的和一分為二
執行任務,計算33到35的和,結果是:102
執行任務,計算36到38的和,結果是:111
切割任務:將39到50的和一分為二
切割任務:將39到44的和一分為二
執行任務,計算39到41的和,結果是:120
執行任務,計算42到44的和,結果是:129
切割任務:將45到50的和一分為二
執行任務,計算45到47的和,結果是:138
執行任務,計算48到50的和,結果是:147
切割任務:將51到100的和一分為二
切割任務:將51到75的和一分為二
切割任務:將51到63的和一分為二
切割任務:將51到57的和一分為二
切割任務:將51到54的和一分為二
執行任務,計算51到52的和,結果是:103
執行任務,計算53到54的和,結果是:107
執行任務,計算55到57的和,結果是:168
切割任務:將58到63的和一分為二
執行任務,計算58到60的和,結果是:177
執行任務,計算61到63的和,結果是:186
切割任務:將64到75的和一分為二
切割任務:將64到69的和一分為二
執行任務,計算64到66的和,結果是:195
執行任務,計算67到69的和,結果是:204
切割任務:將70到75的和一分為二
執行任務,計算70到72的和,結果是:213
執行任務,計算73到75的和,結果是:222
切割任務:將76到100的和一分為二
切割任務:將76到88的和一分為二
切割任務:將76到82的和一分為二
切割任務:將76到79的和一分為二
執行任務,計算76到77的和,結果是:153
執行任務,計算78到79的和,結果是:157
執行任務,計算80到82的和,結果是:243
切割任務:將83到88的和一分為二
執行任務,計算83到85的和,結果是:252
執行任務,計算86到88的和,結果是:261
切割任務:將89到100的和一分為二
切割任務:將89到94的和一分為二
執行任務,計算89到91的和,結果是:270
執行任務,計算92到94的和,結果是:279
切割任務:將95到100的和一分為二
執行任務,計算95到97的和,結果是:288
執行任務,計算98到100的和,結果是:297
最終的計算結果:5050
ForkJoin框架的核心API
ForkJoin框架的核心是ForkJoinPool線程池。該線程池使用一個無鎖的棧來管理空閑線程。如果一個工作線程暫時取不到可用的任務,則可能被掛起,而掛起的線程將被壓入由ForkJoinPool維護的棧中,待有新任務到來時,再從棧中喚醒這些線程。
ForkJoinPool構造器
public ForkJoinPool(int parallelism,
ForkJoinWorkerThreadFactory factory,
UncaughtExceptionHandler handler,
boolean asyncMode) {
this(checkParallelism(parallelism),
checkFactory(factory),
handler,
asyncMode ? FIFO_QUEUE : LIFO_QUEUE,
"ForkJoinPool-" + nextPoolId() + "-worker-");
checkPermission();
}
以上構造器4個參數具體如下:
parallelism:可并行級別。此參數決定框架內并行執行的線程數量。并行的每一個任務都會有一個線程處理,該屬性不是框架中最大的線程數量,和框架可存在的線程數量并不是絕對關聯的。
factory:線程創建工廠。ForkJoin框架創建一個新的線程時,同樣會用到線程創建工廠。只不過這個線程工廠不再需要實現ThreadFactory接口,而是需要實現ForkJoinWorkerThreadFactory接口,它是一個函數式接口,只需要實現一個名為newThread的方法。ForkJoin框架有一個默認的接口實現DefaultForkJoinWorkerThreadFactory。
handler:異常捕獲處理程序。當執行的任務中出現異常,并從任務中被拋出時,就會被handler捕獲。
asyncMode:異步模式。該參數表示任務是否為異步模式,默認為false。asyncMode為true,表示子任務的執行組訓FIFO順序,并且子任務不能被合并;如果asyncMode為false,就表示子任務的執行遵循LIFO順序,子任務可以被合并。asyncMode異步模式僅指任務的調度方式。
ForkJoinPool無參數的、默認的構造器如下:
public ForkJoinPool() {
this(Math.min(MAX_CAP, Runtime.getRuntime().availableProcessors()),
defaultForkJoinWorkerThreadFactory, null, false);
}
該構造器的parallelism值為CPU核數,factory值為defaultForkJoinWorkerThreadFactory默認的線程工廠,異常捕獲處理程序handler值為null,表示不會進行異常處理,異步模式asyncMode值為false,使用LIFO,可以合并子任務的模式。
ForkJoinPool的common通用池
調用 ForkJoinPool.commonPool 方法可以獲取該 ForkJoin 線程池,該線程池通過 makeCommonPool 來構造,具體代碼如下:
private static ForkJoinPool makeCommonPool() {
int parallelism = -1;
ForkJoinWorkerThreadFactory factory = null;
UncaughtExceptionHandler handler = null;
try { // ignore exceptions in accessing/parsing properties
String pp = System.getProperty
("java.util.concurrent.ForkJoinPool.common.parallelism");
String fp = System.getProperty
("java.util.concurrent.ForkJoinPool.common.threadFactory");
String hp = System.getProperty
("java.util.concurrent.ForkJoinPool.common.exceptionHandler");
if (pp != null)
parallelism = Integer.parseInt(pp);
if (fp != null)
factory = ((ForkJoinWorkerThreadFactory)ClassLoader.
getSystemClassLoader().loadClass(fp).newInstance());
if (hp != null)
handler = ((UncaughtExceptionHandler)ClassLoader.
getSystemClassLoader().loadClass(hp).newInstance());
} catch (Exception ignore) {
}
if (factory == null) {
if (System.getSecurityManager() == null)
factory = defaultForkJoinWorkerThreadFactory;
else // use security-managed default
factory = new InnocuousForkJoinWorkerThreadFactory();
}
if (parallelism < 0 && // default 1 less than #cores
(parallelism = Runtime.getRuntime().availableProcessors() - 1) <= 0)
parallelism = 1;
if (parallelism > MAX_CAP)
parallelism = MAX_CAP;
return new ForkJoinPool(parallelism, factory, handler, LIFO_QUEUE,
"ForkJoinPool.commonPool-worker-");
}
使用common池的優點是可以通過指定系統屬性的方式定義“并行度、線程工廠和異常處理類”,并且common池使用的是同步模式,也就是說可以支持任務合并。
通過系統屬性指定parallelism值的示例:
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "8");
向ForkJoinPool線程池提交任務的方式
可以向ForkJoinPool線程池提交一下兩類任務:
- 外部任務(External/Submissions Task)提交。向ForkJoinPool 提交外部任務有三種方式:
- 調用invoke方法,該方法提交任務后線程會等待,等到任務計算完畢返回結果。
- 調用execute方法提交一個任務來異步執行,無返回結果。
- 調用submit方法提交一個任務,并且會返回一個ForkJoinTask實例,之后適當的時候可通過ForkJoinTask實例獲取執行結果。
- 子任務(Worker Task)提交。由任務實例的fork方法完成。當任務被分割之后,內部會調用ForkJoinPool.WorkQueue.push方法直接把任務放到內部隊列匯總等待被執行。
工作竊取算法
ForkJoinPool線程池的任務分為“外部任務”和“內部任務”,兩種任務的存放位置不同:
- 外部任務放在 ForkJoinPool 的全局隊列中。
- 子任務會作為“內部任務”放到內部隊列中,ForkJoinPool池中的每個線程都維護著一個內部隊列,用于存放這些“內部任務”。
由于ForkJoinPool線程池通常有多個工作線程,與之相對應的就會有多個任務隊列,這就會出現任務分配不均衡的問題。工作竊取算法就是幫忙將任務從繁忙的線程分攤給空閑的線程。
工作竊取算法的核心思想是,工作線程自己的活干完了之后,會去看看其它線程有沒有沒完成的任務,如果有就拿過來幫忙。工作竊取算法的主要邏輯,每個線程擁有一個雙端隊列,用于存放要執行的任務,當自己的隊列沒有任務時,可以從其它線程的任務隊列中獲取一個任務繼續執行。
為了避免在任務竊取中發生線程安全問題,一種簡單的優化方法是:在線程自己的本地隊列采用LIFO策略,竊取其它任務隊列的任務時采用FIFO策略。簡單來說,就是獲取自己隊列的任務從頭開始,竊取其它隊列的任務從尾開始。
ForkJoin框架的原理
ForkJoin框架的核心原理大致如下:
- ForkJoin框架的線程池ForkJoinPool的任務分為“外部任務”和“內部任務”。
- “外部任務”放在ForkJoinPool的全局隊列中。
- ForkJoinPool池中的每個線程都維護著一個任務隊列,用于存放“內部任務”,線程切割任務得到的子任務會作為“內部任務”放到內部隊列中。
- 當工作線程想要拿到子任務的計算結果時,先判斷子任務有沒有完成,如果沒有完成,在判斷子任務有沒有被其他線程“竊取”,如果子任務沒有被竊取,則有本線程來完成;一旦子任務被竊取了,就去執行本線程“內部隊列”的其它任務,或者掃描其它的任務隊列并竊取任務。
- 當工作線程完成其“內部任務”,處于空閑狀態時,就會掃描其它的任務隊列竊取任務,盡可能不會阻塞等待。
工作竊取算法的優點如下:
- 線程不會因為等待某個子任務的執行或者沒有內部任務要執行而被阻塞等待、掛起,而是會掃描所有隊列竊取任務,直到所有隊列都為空時才會被掛起。
- ForkJoin框架為每個線程維護者一個內部任務隊列以及一個全局的任務隊列,而且任務隊列都是雙向隊列,可從首尾兩端來獲取任務,極大減少競爭的可能性,提高并行的性能。
生產者-消費者模式
生產者-消費者模式是一個經典的多線程設計模式,它為多線程間的協作提供了良好的解決方案,是高并發編程過程匯總常用的一種設計模式。
產生數據的模塊可以成為生產者,消費數據的模塊可以稱為消費者。還需要一個數據緩沖區作為生產者和消費者之間的中介,使他們解耦。
Future 模式
Future模式的核心思想是異步調用。它不會立即返回我們需要的數據,而是返回一個契約(或異步任務),將來憑借這個契約(或異步任務)獲取結果。
下面是一個客戶端調服務端的示例。實現了Future模式的客戶端在調用服務端得到返回結果后并不急于對其進行處理而是調用其他業務邏輯,充分利用等待時間,這就是Future模式的核心所在。在完成其他業務處理后,最后再使用返回比較慢的Future數據。這樣在整個調用中就不存在無謂的等待,充分利用所有的時間片,從而提高了系統響應速度。
在實現上,Future模式和異步回調模式既有區別,又有倆你洗。Java的Future實現類并沒有支持異步回調,任然需要主動獲取耗時任務的結果;而Java 8 中的CompletableFuture組件實現了異步回調模式。