Java多執行緒通過多核CPU來提升速度--更快的執行
全文翻譯自15L大神
方案1:單執行緒
假設有個請求,這個請求服務端的處理需要執行3個很緩慢的IO操作(比如資料庫查詢或檔案查詢),那麼正常的順序可能是(括號裡面代表執行時間):
a、讀取檔案1 (10ms)
b、處理1的資料(1ms)
c、讀取檔案2 (10ms)
d、處理2的資料(1ms)
e、讀取檔案3 (10ms)
f、處理3的資料(1ms)
g、整合1、2、3的資料結果 (1ms)
單執行緒總共就需要34ms。
package cn.bellychang.d0209; public class NoConcurrence { /** * @param args * @throws InterruptedException */ public static void main(String[] args) throws InterruptedException { long start = System.currentTimeMillis(); int a,b,c; //讀取檔案1 Thread.sleep(10L); //處理1的資料 Thread.sleep(1L); a = 1; //讀取檔案2 Thread.sleep(10L); //處理2的資料 Thread.sleep(1L); b =1; //讀取檔案3 Thread.sleep(10L); //處理3的資料 Thread.sleep(1L); //整合1、2、3的資料結果 Thread.sleep(1L); c = 1; int result = a + b + c; long end = System.currentTimeMillis(); System.out.println(end - start); } }
方案2:如果你在這個請求內,把ab、cd、ef分別分給3個執行緒去做,就只需要12ms了。
package cn.bellychang.d0209; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; class ThreadA implements Callable<Integer> { @Override public Integer call() throws Exception { // 讀取檔案1 Thread.sleep(10L); // 處理1的資料 Thread.sleep(1L); return 1; } } class ThreadB implements Callable<Integer> { @Override public Integer call() throws Exception { // 讀取檔案2 Thread.sleep(10L); // 處理2的資料 Thread.sleep(1L); return 1; } } class ThreadC implements Callable<Integer> { @Override public Integer call() throws Exception { // 讀取檔案2 Thread.sleep(10L); // 處理2的資料 Thread.sleep(1L); return 1; } } public class ConcurenceForMultiCPU { /** * @param args * @throws InterruptedException * @throws ExecutionException */ public static void main(String[] args) throws InterruptedException, ExecutionException { long start = System.currentTimeMillis(); ExecutorService exec = Executors.newCachedThreadPool(); Future<Integer> f = exec.submit(new ThreadA()); Future<Integer> g = exec.submit(new ThreadB()); Future<Integer> h = exec.submit(new ThreadC()); int result = f.get()+g.get()+h.get(); long end = System.currentTimeMillis(); System.out.println(end - start); } }
所以多執行緒不是沒怎麼用,而是,你平常要善於發現一些可優化的點。然後評估方案是否應該使用。
方案3:
假設還是上面那個相同的問題:但是每個步驟的執行時間不一樣了。
a、讀取檔案1 (1ms)
b、處理1的資料(1ms)
c、讀取檔案2 (1ms)
d、處理2的資料(1ms)
e、讀取檔案3 (28ms)
f、處理3的資料(1ms)
g、整合1、2、3的資料結果 (1ms)
單執行緒總共就需要34ms。
package cn.bellychang.d0209; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; class ThreadA1 implements Callable<Integer> { @Override public Integer call() throws Exception { // 讀取檔案1 Thread.sleep(1L); // 處理1的資料 Thread.sleep(1L); return 1; } } class ThreadB1 implements Callable<Integer> { @Override public Integer call() throws Exception { // 讀取檔案2 Thread.sleep(1L); // 處理2的資料 Thread.sleep(1L); return 1; } } class ThreadC1 implements Callable<Integer> { @Override public Integer call() throws Exception { // 讀取檔案2 Thread.sleep(28L); // 處理2的資料 Thread.sleep(1L); return 1; } } public class ConcurenceForMultiCPU1 { /** * @param args * @throws InterruptedException * @throws ExecutionException */ public static void main(String[] args) throws InterruptedException, ExecutionException { long start = System.currentTimeMillis(); ExecutorService exec = Executors.newCachedThreadPool(); Future<Integer> f = exec.submit(new ThreadA1()); Future<Integer> g = exec.submit(new ThreadB1()); Future<Integer> h = exec.submit(new ThreadC1()); int result = f.get()+g.get()+h.get(); long end = System.currentTimeMillis(); System.out.println(end - start); } }
如果還是按上面的劃分方案(上面方案和木桶原理一樣,耗時取決於最慢的那個執行緒的執行速度),在這個例子中是第三個執行緒,執行29ms。那麼最後這個請求耗時是30ms。比起不用單執行緒,就節省了4ms。但是有可能執行緒排程切換也要花費個1、2ms。因此,這個方案顯得優勢就不明顯了,還帶來程式複雜度提升。不太值得。
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
那麼現在優化的點,就不是第一個例子那樣的任務分割多執行緒完成。而是優化檔案3的讀取速度。
可能是採用快取和減少一些重複讀取。
首先,假設有一種情況,所有使用者都請求這個請求,那其實相當於所有使用者都需要讀取檔案3。那你想想,100個人進行了這個請求,相當於你花在讀取這個檔案上的時間就是28×100=2800ms了。那麼,如果你把檔案快取起來,那隻要第一個使用者的請求讀取了,第二個使用者不需要讀取了,從記憶體取是很快速的,可能1ms都不到。
public class MyServlet extends Servlet{
private static Map<String, String> fileName2Data = new HashMap<String, String>();
private void processFile3(String fName){
String data = fileName2Data.get(fName);
if(data==null){
data = readFromFile(fName); //耗時28ms
fileName2Data.put(fName, data);
}
//process with data
}
}
看起來好像還不錯,建立一個檔名和檔案資料的對映。如果讀取一個map中已經存在的資料,那麼就不不用讀取檔案了。
可是問題在於,Servlet是併發,上面會導致一個很嚴重的問題,死迴圈。因為,HashMap在併發修改的時候,可能是導致迴圈連結串列的構成!!!(具體你可以自行閱讀HashMap原始碼)如果你沒接觸過多執行緒,可能到時候發現伺服器沒請求也巨卡,也不知道什麼情況!
好的,那就用ConcurrentHashMap,正如他的名字一樣,他是一個執行緒安全的HashMap,這樣能輕鬆解決問題。
public class MyServlet extends Servlet{
private static ConcurrentHashMap<String, String> fileName2Data = new ConcurrentHashMap<String, String>();
private void processFile3(String fName){
String data = fileName2Data.get(fName);
if(data==null){
data = readFromFile(fName); //耗時28ms
fileName2Data.put(fName, data);
}
//process with data
}
}
這樣真的解決問題了嗎,這樣雖然只要有使用者訪問過檔案a,那另一個使用者想訪問檔案a,也會從fileName2Data中拿資料,然後也不會引起死迴圈。
可是,如果你覺得這樣就已經完了,那你把多執行緒也想的太簡單了,騷年!
你會發現,1000個使用者首次訪問同一個檔案的時候,居然讀取了1000次檔案(這是最極端的,可能只有幾百)。What the fuckin hell!!!
難道程式碼錯了嗎,難道我就這樣過我的一生!
好好分析下。Servlet是多執行緒的,那麼
public class MyServlet extends Servlet{
private static ConcurrentHashMap<String, String> fileName2Data = new ConcurrentHashMap<String, String>();
private void processFile3(String fName){
String data = fileName2Data.get(fName);
//“偶然”-- 1000個執行緒同時到這裡,同時發現data為null
if(data==null){
data = readFromFile(fName); //耗時28ms
fileName2Data.put(fName, data);
}
//process with data
}
}
上面註釋的“偶然”,這是完全有可能的,因此,這樣做還是有問題。
因此,可以自己簡單的封裝一個任務來處理。
public class MyServlet extends Servlet{
private static ConcurrentHashMap<String, FutureTask> fileName2Data = new ConcurrentHashMap<String, FutureTask>();
private static ExecutorService exec = Executors.newCacheThreadPool();
private void processFile3(String fName){
FutureTask data = fileName2Data.get(fName);
//“偶然”-- 1000個執行緒同時到這裡,同時發現data為null
if(data==null){
data = newFutureTask(fName);
FutureTask old = fileName2Data.putIfAbsent(fName, data);
if(old==null){
data = old;
}else{
exec.execute(data);
}
}
String d = data.get();
//process with data
}
private FutureTask newFutureTask(final String file){
return new FutureTask(new Callable<String>(){
public String call(){
return readFromFile(file);
}
private String readFromFile(String file){return "";}
}
}
}