Java併發執行任務的幾種方式
背景
在編寫業務程式碼時經常遇到併發執行多個任務的需求,因為序列執行太慢,會影響業務程式碼效能。特別對於直接面向普通使用者的業務來說使用者體驗至關重要,保證使用者體驗重要的一點是要“快”。業務程式碼中經常需要呼叫其它業務介面或者同時從多個數據源取資料再處理等,這種情況下勢必要走網路請求,網路消耗必不可少,最好的情況是毫秒級別,一般情況下是幾十毫秒級別,甚至幾百毫秒,TimeoutException恐怕大家並不陌生。
例子
當你瀏覽微信朋友圈時,微信會把你朋友最近的動態展示在你朋友圈裡,並且按照時間順序最近的排在前面。你會發現,朋友圈裡不會展示把你刪除了或者你把他刪除了的好友,不會展示被你設定過“不看他(她)的朋友圈”的好友或者對方把你設定過“不讓他(她)看我的朋友圈”的好友,不會展示被你拉黑或把你拉黑的好友,不會展示被微信系統標記為spam的好友,等等。對於微信這種支援數億人聊天的應用,其系統必定很複雜,解耦做得也比較好。
下面模擬下這個介面的實現:
public List<Feed> lastestFeeds(String wxId) {
//序列執行
//1. 獲取你得好友列表
//2. 去掉把你刪除的好友
//3. 去掉被你刪除的好友
//4. 去掉被你設定過"不看他(她)的朋友圈"的好友
//5. 去掉對你設定過"不讓他(她)看我的朋友圈"的好友
//6. 去掉被你拉黑和把你拉黑的好友
//7. 去掉被微信系統標記為作弊的好友
//...
//8. 獲取好友最近動態再返回
}
對於微信這種複雜的系統,通常不可能從一個介面獲取到這些資訊,必須從多個介面獲取到這些資訊後再處理。如果說序列實現這些功能,你可以想象一下是不是慢到吐血,相信微信也不會這麼幹,否則朋友圈會刷半天也沒響應,那麼這個使用者體驗就太糟糕了。那麼這個時候併發執行這些子任務就可以很高效的處理掉這種情況。具體到這個介面也就是會把1-7拆解成單個子任務,再丟到執行緒池非同步的執行。最後執行完了,再彙總處理。
public List<Feed> lastestFeedsV2(String wxId) {
//併發執行,無先後先後執行
//1. 獲取你得好友列表
//2. 去掉把你刪除的好友
//3. 去掉被你刪除的好友
//4. 去掉被你設定過"不看他(她)的朋友圈"的好友
//5. 去掉對你設定過"不讓他(她)看我的朋友圈"的好友
//6. 去掉被你拉黑和把你拉黑的好友
//7. 去掉被微信系統標記為作弊的好友
//...
//等待所有子任務完成,彙總處理
//8. 獲取好友最近動態再返回
}
那麼如何實現併發執行呢,下面討論幾種實現。
CountDownLatch.await() VS ExecutorService.invokeAll()
從功能上講這兩者均可以實現併發執行多個任務並等待的功能。先看程式碼如何完成以上功能:
CountDownLatch實現:
public List<Feed> lastestFeeds(String wxId) {
ThreadPoolExecutor executor = ...;
CountDownLatch latch = new CountDownLatch(7);
executor.execute(new Runnable() {
@Override
public void run() {
try{
//1. 獲取你得好友列表
} catch (Exception e) {
} finally {
latch.countDown();
}
}
});
executor.execute(new Runnable() {
@Override
public void run() {
try{
//2. 去掉把你刪除的好友
} catch (Exception e) {
} finally {
latch.countDown();
}
}
});
executor.execute(new Runnable() {
@Override
public void run() {
try{
//3. 去掉被你刪除的好友
} catch (Exception e) {
} finally {
latch.countDown();
}
}
});
executor.execute(new Runnable() {
@Override
public void run() {
try{
//4. 去掉被你設定過"不看他(她)的朋友圈"的好友
} catch (Exception e) {
} finally {
latch.countDown();
}
}
});
executor.execute(new Runnable() {
@Override
public void run() {
try{
//5. 去掉對你設定過"不讓他(她)看我的朋友圈"的好友
} catch (Exception e) {
} finally {
latch.countDown();
}
}
});
executor.execute(new Runnable() {
@Override
public void run() {
try{
//6. 去掉被你拉黑和把你拉黑的好友
} catch (Exception e) {
} finally {
latch.countDown();
}
}
});
executor.execute(new Runnable() {
@Override
public void run() {
try{
//7. 去掉被微信系統標記為作弊的好友
} catch (Exception e) {
} finally {
latch.countDown();
}
}
});
try {
//latch.await();
latch.await(500, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
//等待所有子任務完成,彙總處理
//8. 獲取好友最近動態再返回
}
ExecutorService實現:
public List<Feed> lastestFeeds(String wxId) {
ThreadPoolExecutor executor = ...;
CountDownLatch latch = new CountDownLatch(7);
List<Callable<String>> tasks = new ArrayList<>(7);
tasks.add(new Callable<String>() {
@Override
public String call() throws Exception {
//1. 獲取你得好友列表
return ...;
}
});
tasks.add(new Callable<String>() {
@Override
public String call() throws Exception {
//2. 去掉把你刪除的好友
return ...;
}
});
tasks.add(new Callable<String>() {
@Override
public String call() throws Exception {
//3. 去掉被你刪除的好友
return ...;
}
});
tasks.add(new Callable<String>() {
@Override
public String call() throws Exception {
//4. 去掉被你設定過"不看他(她)的朋友圈"的好友
return ...;
}
});
tasks.add(new Callable<String>() {
@Override
public String call() throws Exception {
//5. 去掉對你設定過"不讓他(她)看我的朋友圈"的好友
return ...;
}
});
tasks.add(new Callable<String>() {
@Override
public String call() throws Exception {
//6. 去掉被你拉黑和把你拉黑的好友
return ...;
}
});
tasks.add(new Callable<String>() {
@Override
public String call() throws Exception {
//7. 去掉被微信系統標記為作弊的好友
return ...;
}
});
try {
// List<Future<String>> futureList = executor.invokeAll(tasks);
List<Future<String>> futureList = executor.invokeAll(tasks, 500, TimeUnit.MILLISECONDS);
for(Future<String> future : futureList) {
if(future.isCancelled()) {
//處理
}
try {
future.get();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
區別
兩者實現的區別是什麼?從效能上講應該是沒有太大差異,在此要感謝Doug Lea大神,感謝他優雅的實現,具體的實現,請各位看官自行百度和google。
我主要是談談這兩者使用上的細微差異,可以根據具體業務場景選擇一個更合適的。兩者在等待子任務結束時,均提供了限時和不限時版本。
如果使用限時版本
關鍵的差異在於CountDownLatch超時後不再阻塞主執行緒,而是會繼續執行,對於沒有完成的子任務,它不知道也不會處理,也就是說沒有完成的子任務還是會繼續執行。而ExecutorService.invokeAll超時後,會取消所有線上程池任務佇列中等待執行的子任務,說白了就是超時的子任務會被取消。
對於我舉的微信這個例子來說,主執行緒等待超時後,子任務再返回資料是沒有意義的,繼續執行超時的子任務只會浪費CPU而已,尤其是對於QPS較大的業務來說,影響更明顯。這種情況下選擇ExecutorService.invokeAll可能更好些,當然如果你提供額外的狀態標識如定義個AtomicBoolean來輔助實現超時不執行子任務的功能也是可以的。當然有些場景,即便主執行緒超時了,子任務也必須執行(例如子任務中涉及資料儲存,否則資料可能丟失),這種情況下,應該是使用CountDownLatch。
希望讀者諸君能夠認真體會這點差異。
對於不限時版本
兩者無太大差異,但在生產環境中要慎用,因為它不可控。
其它
另外再一點要注意的是,使用CountDownLatch時子任務的結果需要通過執行緒安全的容器收集如ConcurrentHashMap等,再處理。ExecutorService.invokeAll因為返回的是List,所以可以拿到結果,但如果想通過Future找到是哪個子任務,就只能根據順序來確認了。
相關推薦
Java併發執行任務的幾種方式
背景 在編寫業務程式碼時經常遇到併發執行多個任務的需求,因為序列執行太慢,會影響業務程式碼效能。特別對於直接面向普通使用者的業務來說使用者體驗至關重要,保證使用者體驗重要的一點是要“快”。業務程式碼中經常需要呼叫其它業務介面或者同時從多個數據源取資料再處理等,
java實現同步的幾種方式(總結)
副本 增刪改 否則 都是 fin ret 語義 value art 為何要使用同步? java允許多線程並發控制,當多個線程同時操作一個可共享的資源變量時(如數據的增刪改查), 將會導致數據不準確,相互之間產生沖突,因此加入同步鎖以避免在該線程沒有完成操
python利用unittest進行測試用例執行的幾種方式
尋找 顯示 成員 使用方式 main down 測試的 支持 ase 利用python進行測試時,測試用例的加載方式有2種: 一種是通過unittest.main()來啟動所需測試的測試模塊; 一種是添加到testsuite集合中再加載所有的被測試對象,而test
js中頁面加載完成後執行的幾種方式及執行順序
class 事件 兩種 在哪裏 log 等待 沒有 cti 完成 1:使用jQuery的$(function){}; 2:使用jquery的$(document).ready(function(){});前兩者本質上沒有區別,第1種是第2種的簡寫方式。兩個是document
java開發webservice的幾種方式(轉)
java開發webservice的幾種方式(轉) webservice的應用已經越來越廣泛了,下面介紹幾種在Java體系中開發webservice的方式,相當於做個記錄。 1.Axis2 Axis是apache下一個開源的webservice開發元件,出現的算是比較早了,也
java 字串擷取的幾種方式
java 字串擷取的幾種方式 1.split()+正則表示式來進行擷取。 將正則傳入split()。返回的是一個字串陣列型別。不過通過這種方式擷取會有很大的效能損耗,因為分析正則非常耗時。 String str = "abc,12,3yy98,0"; String[] strs
Java建立物件的幾種方式。
Java建立物件的幾種方式(重要): (1) 用new語句建立物件,這是最常見的建立物件的方法。 (2) 運用反射手段,呼叫java.lang.Class或者java.lang.reflect.Constructor類的newInstance()例項方法。 (3) 呼叫物件的clone()方法。 (4) 運用
java字串拼接的幾種方式
1. plus方式 當左右兩個量其中有一個為String型別時,用plus方式可將兩個量轉成字串並拼接。 String a="";int b=0xb;String c=a+b;2. concat方式 當兩個量都為String型別且值不為null時,可以用concat方式。 String a="a";S
VSCode的Python擴充套件下程式執行的幾種方式與環境變數管理
在VSCode中編寫Python程式時,由於有些地方要使用環境變數,但是發現設定的環境變數有時不起作用,花了點時間研究了一下,過程不表,直接說結論。 首先,環境變數的設定,Python擴充套件中有三種方式: 直接設定系統環境變數,或在使用命令列啟動VSCode時臨時先設定環境變數。這種方式設定的環境變
linux指令碼中父shell與子shell 執行的幾種方式
本文主要介紹以下幾個命令的區別: shell subshell source $ (commond) `commond` Linux執行Scripts有兩種方式,主要區別在於是否建立subshell 1. source filename or . filename 不建立subshell,在當前shel
java檔案下載的幾種方式
轉載地址 1.通過流下載 public void download(String id, HttpServletResponse response) { response.setHeader("Access-Control-Allow-Origin",
Java建立陣列的幾種方式
借鑑http://blog.csdn.net/u014199097/article/details/50551731 1、一維陣列的宣告 T[] arrayName; 或 T arrayName[]; 附:推薦使用第一種格式,因為第一種格式具有更好的可讀性,表示T[]是一
Java下載檔案的幾種方式
1.以流的方式下載 public HttpServletResponse download(String path, HttpServletResponse response) { try { // path是指欲下載的檔案的路徑。
關於JAVA呼叫C++的幾種方式和一些問題 UnsatisfiedLinkError
關於JAVA呼叫C++的幾種方式和一些問題 java呼叫c++有幾種方式,1.JNA方式,2,JNative 方式,3.JNI 方式。: 1.JNA方式 public interface MyCLibrary extends Library {
Java中定義常量幾種方式
在開發中定義常量是很常見的事,但常量定義有哪幾種方式可選?各種定義方式有什麼優缺點?咱們就用這篇小文來梳理下^_^ 1.通過介面Interface來定義(不推薦) 定義方式如下: 我們可以這樣使用它: 這種定義方式的優點: 適合懶人使用,為什麼呢?
android延時執行的幾種方式
在專案中有很多的方法可能我們不需要立即執行,那麼就需要延時。 首先第一種就是執行緒: sleep會阻塞執行緒 new Thread (new Runnable(){ public void run(){ Thread.sleep(time
啟動執行緒幾種方式
一:建立執行緒的幾種方式 1.第一種方法是將類宣告為 Thread 的子類。該子類應重寫 Thread 類的 run 方法,然後在run方法裡填寫相應的邏輯程式碼。 class ThreadDemo1 extends Thread{ @O
Linux 程序後臺執行的幾種方式 screen
screen是Linux視窗管理器,使用者可以建立多個screen會話,每個screen會話又可以建立多個window視窗,每一個視窗就像一個可操作的真實的ssh終端一樣。 安裝 screen
java整合groovy的幾種方式對比
Groovy的幾種整合方式:groovyshell、GroovyClassLoader、GroovyScriptEngine,其中groovyshell的方式不支援指令碼快取,會導致垃圾回收頻繁