1. 程式人生 > >Java多執行緒通過多核CPU來提升速度--更快的執行

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 "";}
        }
    }
}