1. 程式人生 > >【好文】淘寶面試題:如何充分利用多核CPU,計算很大的List中所有整數的和

【好文】淘寶面試題:如何充分利用多核CPU,計算很大的List中所有整數的和

引用

前幾天在網上看到一個淘寶的面試題:有一個很大的整數list,需要求這個list中所有整數的和,寫一個可以充分利用多核CPU的程式碼,來計算結果。

一:分析題目
從題中可以看到“很大的List”以及“充分利用多核CPU”,這就已經充分告訴我們要採用多執行緒(任務)進行編寫。具體怎麼做呢?大概的思路就是分割List,每一小塊的List採用一個執行緒(任務)進行計算其和,最後等待所有的執行緒(任務)都執行完後就可得到這個“很大的List”中所有整數的和。
二:具體分析和技術方案
既然我們已經決定採用多執行緒(任務),並且還要分割List,每一小塊的List採用一個執行緒(任務)進行計算其和,那麼我們必須要等待所有的執行緒(任務)完成之後才能得到正確的結果,那麼怎麼才能保證“等待所有的執行緒(任務)完成之後輸出結果呢”?這就要靠java.util.concurrent包中的CyclicBarrier類了。它是一個同步輔助類,它允許一組執行緒(任務)互相等待,直到到達某個公共屏障點 (common barrier point)。在涉及一組固定大小的執行緒(任務)的程式中,這些執行緒(任務)必須不時地互相等待,此時 CyclicBarrier 很有用。簡單的概括其適應場景就是:當一組執行緒(任務)併發的執行一件工作的時候,必須等待所有的執行緒(任務)都完成時才能進行下一個步驟。具體技術方案步驟如下:

  • 分割List,根據採用的執行緒(任務)數平均分配,即list.size()/threadCounts。
  • 定義一個記錄“很大List”中所有整數和的變數sum,採用一個執行緒(任務)處理一個分割後的子List,計運算元List中所有整數和(subSum),然後把和(subSum)累加到sum上。
  • 等待所有執行緒(任務)完成後輸出總和(sum)的值。


示意圖如下:

三:詳細編碼實現
程式碼中有很詳細的註釋,這裡就不解釋了。

/**
 * 計算List中所有整數的和<br>
 * 採用多執行緒,分割List計算
 * @author 飛雪無情
 * @since 2010-7-12
 */
public class CountListIntegerSum {
	private long sum;//存放整數的和
	private CyclicBarrier barrier;//障柵集合點(同步器)
	private List<Integer> list;//整數集合List
	private int threadCounts;//使用的執行緒數
	public CountListIntegerSum(List<Integer> list,int threadCounts) {
		this.list=list;
		this.threadCounts=threadCounts;
	}
	/**
	 * 獲取List中所有整數的和
	 * @return
	 */
	public long getIntegerSum(){
		ExecutorService exec=Executors.newFixedThreadPool(threadCounts);
		int len=list.size()/threadCounts;//平均分割List
		//List中的數量沒有執行緒數多(很少存在)
		if(len==0){
			threadCounts=list.size();//採用一個執行緒處理List中的一個元素
			len=list.size()/threadCounts;//重新平均分割List
		}
		barrier=new CyclicBarrier(threadCounts+1);
		for(int i=0;i<threadCounts;i++){
			//建立執行緒任務
			if(i==threadCounts-1){//最後一個執行緒承擔剩下的所有元素的計算
				exec.execute(new SubIntegerSumTask(list.subList(i*len,list.size())));
			}else{
				exec.execute(new SubIntegerSumTask(list.subList(i*len, len*(i+1)>list.size()?list.size():len*(i+1))));
			}
		}
		try {
			barrier.await();//關鍵,使該執行緒在障柵處等待,直到所有的執行緒都到達障柵處
		} catch (InterruptedException e) {
			System.out.println(Thread.currentThread().getName()+":Interrupted");
		} catch (BrokenBarrierException e) {
			System.out.println(Thread.currentThread().getName()+":BrokenBarrier");
		}
		exec.shutdown();
		return sum;
	}
	/**
	 * 分割計算List整數和的執行緒任務
	 * @author lishuai
	 *
	 */
	public class SubIntegerSumTask implements Runnable{
		private List<Integer> subList;
		public SubIntegerSumTask(List<Integer> subList) {
			this.subList=subList;
		}
		public void run() {
			long subSum=0L;
			for (Integer i : subList) {
				subSum += i;
			}  
			synchronized(CountListIntegerSum.this){//在CountListIntegerSum物件上同步
				sum+=subSum;
			}
			try {
				barrier.await();//關鍵,使該執行緒在障柵處等待,直到所有的執行緒都到達障柵處
			} catch (InterruptedException e) {
				System.out.println(Thread.currentThread().getName()+":Interrupted");
			} catch (BrokenBarrierException e) {
				System.out.println(Thread.currentThread().getName()+":BrokenBarrier");
			}
			System.out.println("分配給執行緒:"+Thread.currentThread().getName()+"那一部分List的整數和為:\tSubSum:"+subSum);
		}
		
	}
	
}

四:總結
本文主要通過一個淘寶的面試題為引子,介紹了併發的一點小知識,主要是介紹通過CyclicBarrier同步輔助器輔助多個併發任務共同完成一件工作。Java SE5的java.util.concurrent引入了大量的設計來解決併發問題,使用它們有助於我們編寫更加簡單而健壯的併發程式。

附mathfox提到的ExecutorService.invokeAll()方法的實現
這個不用自己控制等待,invokeAll執行給定的任務,當所有任務完成時,返回保持任務狀態和結果的 Future 列表。sdh5724也說用了同步,效能不好。這個去掉了同步,根據返回結果的 Future 列表相加就得到總和了。

/**
 * 使用ExecutorService的invokeAll方法計算
 * @author 飛雪無情
 *
 */
public class CountSumWithCallable {

	/**
	 * @param args
	 * @throws InterruptedException 
	 * @throws ExecutionException 
	 */
	public static void main(String[] args) throws InterruptedException, ExecutionException {
		int threadCounts =19;//使用的執行緒數
		long sum=0;
		ExecutorService exec=Executors.newFixedThreadPool(threadCounts);
		List<Callable<Long>> callList=new ArrayList<Callable<Long>>();
		//生成很大的List
		List<Integer> list = new ArrayList<Integer>();
		for (int i = 0; i <= 1000000; i++) {
			list.add(i);
		}
		int len=list.size()/threadCounts;//平均分割List
		//List中的數量沒有執行緒數多(很少存在)
		if(len==0){
			threadCounts=list.size();//採用一個執行緒處理List中的一個元素
			len=list.size()/threadCounts;//重新平均分割List
		}
		for(int i=0;i<threadCounts;i++){
			final List<Integer> subList;
			if(i==threadCounts-1){
				subList=list.subList(i*len,list.size());
			}else{
				subList=list.subList(i*len, len*(i+1)>list.size()?list.size():len*(i+1));
			}
			//採用匿名內部類實現
			callList.add(new Callable<Long>(){
				public Long call() throws Exception {
					long subSum=0L;
					for(Integer i:subList){
						subSum+=i;
					}
					System.out.println("分配給執行緒:"+Thread.currentThread().getName()+"那一部分List的整數和為:\tSubSum:"+subSum);
					return subSum;
				}
			});
		}
		List<Future<Long>> futureList=exec.invokeAll(callList);
		for(Future<Long> future:futureList){
			sum+=future.get();
		}
		exec.shutdown();
		System.out.println(sum);
	}

}

一些感言
這篇文章是昨天夜裡11點多寫好的,我當時是在網上看到了這個題目,就做了一下分析,寫了實現程式碼,由於水平有限,難免有bug,這裡感謝xifo等人的指正。這些帖子從發表到現在不到24小時的時間裡創造了近9000的瀏覽次數,回覆近100,這是我沒有想到的,javaeye很久沒這麼瘋狂過啦。這不是因為我的演算法多好,而是因為這個題目、這篇帖子所體現出的意義。大家在看完這篇帖子後不光指正錯誤,還對方案進行了改進,關鍵是思考,人的思維是無窮的,只要我們善於發掘,善於思考,總能想出一些意想不到的方案。

從演算法看,或者從題目場景對比程式碼實現來看,或許不是一篇很好的帖子,但是我說這篇帖子是很有意義的,方案也是在很多場景適用,有時我們可以假設這不是計算和,而是把資料寫到一個個的小檔案裡,或者是分割進行網路傳輸等等,都有一定的啟發,特別是回帖中的討論。

單說一下回帖,我建議進來的人儘量看完所有的回帖,因為這裡是很多人集思廣益的精華,這裡有他們分析問題,解決問題的思路,還有每個人提到的解決方案,想想為什麼能用?為什麼不能用?為什麼好?為什麼不好?


我一直相信:討論是解決問題、提高水平的最佳方式!