這兩個月來因為工作和家庭的事情,導致一直都很忙,沒有多少時間去汲取養分,也就沒有什麼產出,最近稍微輕鬆了一點,後續的【進階之路】會慢慢回到正軌。
開門見山的說,第一次接觸到多執行緒處理同一個任務,是使用IO多執行緒下載檔案,之後也一直沒有再處理這一塊的任務,直到前幾天有同事問我,為什麼多執行緒處理一個list集合會出現各種bug,以及如何使用多執行緒的方式處理同一個list集合。
第一、為什麼會出現類似於重複處理某一個模組的問題?
我們都知道,在Java中,每個執行緒都有自己獨立的工作記憶體,執行緒對共享變數的所有操作都必須在自己的工作記憶體中進行,不能直接從主記憶體中讀寫。
如果執行緒1的修改內容想被執行緒2得到,那麼執行緒1工作記憶體中修改後的共享變數需要先重新整理到主記憶體中,再把主記憶體中更新過的共享變數更新到工作記憶體2中。
這個時候一般我們是考慮使用java中各種同步化的方法,首先,因為是需要高效處理list集合,所以可以排除synchronized方法,於是我想到了使用CompletionService操作非同步任務。
大家可以在這篇文章看到具體的詳解:
【進階之路】執行緒池擴充與CompletionService操作非同步任務
一、CompletionService
首先,按照之前文章的方法自定義一個WeedThreadPool
public class WeedThreadPool extends ThreadPoolExecutor {
private final ThreadLocal<Long> startTime =new ThreadLocal<>();
private final Logger log =Logger.getLogger("WeedThreadPool");
//統計執行次數
private final AtomicLong numTasks =new AtomicLong();
//統計總執行時間
private final AtomicLong totalTime =new AtomicLong();
/**
* 這裡是實現執行緒池的構造方法,我隨便選了一個,大家可以根據自己的需求找到合適的構造方法
*/
public WeedThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
}
然後就是實現執行緒池處理list集合的方法
public class WeedExecutorServiceDemo {
BlockingQueue<Runnable> taskQueue;
final static WeedThreadPool weedThreadPool = new WeedThreadPool(3, 10, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(100));
// 開始時間
public static void main(String[] args) throws InterruptedException, ExecutionException {
//記錄任務開始時間
long start = System.currentTimeMillis();
CompletionService<List<Integer>> cs = new ExecutorCompletionService<>(weedThreadPool);
int tb=1;
//生成集合
List<List<Integer>> list1 =new ArrayList();
for (int i = 0; i < 10; i++) {
List<Integer> list =new ArrayList();
//隨機生成任務處理
int hb=tb;
tb =tb*2;
int finalTb = tb;
cs.submit(new Callable<List<Integer>>(){
@Override
public List<Integer> call() throws Exception {
for (int j = hb; j< finalTb; j++){
list.add(j);
}
System.out.println(Thread.currentThread().getName()+"["+list+"]");
return list;
}
});
}
//注意在處理完畢後結束任務
weedThreadPool.shutdown();
for (int i = 0; i < 10; i++) {
Future<List<Integer>> future = cs.take();
if (future != null) {
list1.add(future.get());
System.out.println(future.get());
}
}
System.err.println("執行任務消耗了 :" + (System.currentTimeMillis() - start) + "毫秒");
System.out.println("結果["+list1.size()+"]==="+list1);
}
}
處理結果:
從結果上來看,還是比較美好的,通過CompletionService能夠比較快速地分段處理任務,我之前也有提過,合理的執行緒池大小設計有助於提高任務的處理效率,網上通用的設定方法一般是這樣的:
最佳執行緒數目 = ((執行緒等待時間+執行緒CPU時間)/執行緒CPU時間 )* CPU數目
進而得出
最佳執行緒數目 = (執行緒等待時間與執行緒CPU時間之比 + 1)* CPU數目
二、ForkJoinPool
當然,除了使用CompletionService之外,也可以使用ForkJoinPool來設計一個處理方法。
ForkJoinPool和ThreadPoolExecutor都是繼承自AbstractExecutorService抽象類,所以它和ThreadPoolExecutor的使用幾乎沒有多少區別。其核心思想是將大的任務拆分成多個小任務,然後在將多個小任務處理彙總到一個結果上。
ForkJoinPool框架通過初始化ForkJoinTask來執行任務,並提供了以下兩個子類:
- RecursiveAction:用於沒有返回結果的任務。
- RecursiveTask :用於有返回結果的任務。
我們實現的過程中可以使用RecursiveTask方法來分段處理list集合。
public class RecursiveTaskDemo {
private static final ExecutorService executor = new ThreadPoolExecutor(2, 3, 10, TimeUnit.SECONDS, new LinkedBlockingQueue(10));
private static final int totalRow = 53000;
private static final int splitRow = 10000;
public static void main(String[] args) throws InterruptedException, ExecutionException {
long start = System.currentTimeMillis();
//先迴圈生成待待處理集合
List<Integer> list = new ArrayList<>(totalRow);
for (int i = 0; i < totalRow; i++) {
list.add(i);
}
//計算出需要建立的任務數
int loopNum = (int)Math.ceil((double)totalRow/splitRow);
ForkJoinPool pool = new ForkJoinPool(loopNum);
ForkJoinTask<List> submit = pool.submit(new MyTask(list, 0, list.size()));
List<List<Integer>>list1=new ArrayList<>();
list1.add(submit.get());
System.err.println("執行任務消耗了 :" + (System.currentTimeMillis() - start) + "毫秒");
System.out.println("結果["+list1.size()+"]==="+list1);
}
//繼承RecursiveTask
static class MyTask extends RecursiveTask<List> {
private List<Integer> list;
private int startRow;
private int endRow;
public MyTask(List<Integer> list, int startRow, int endRow) {
this.list = list;
this.startRow = startRow;
this.endRow = endRow;
}
/**
* 遞迴處理資料,計算
* @return
*/
@Override
protected List compute() {
if (endRow - startRow <= splitRow) {
List<Integer> ret = new ArrayList<>();
for (int i = startRow; i < endRow; i++) {
//遞迴處理資料
ret.add(list.get(i));
}
System.out.println(Thread.currentThread().getName()+"["+ret+"]");
return ret;
}
int loopNum = (int)Math.ceil((double)totalRow/splitRow);
int startRow = 0;
List<MyTask> myTaskList = new ArrayList<>();
for (int i = 0; i < loopNum; i++) {
if (startRow > totalRow) {
break;
}
int endRow = Math.min(startRow + splitRow, totalRow);
System.out.println(String.format("startRow:%s, endRow:%s", startRow, endRow));
myTaskList.add(new MyTask(list, startRow, endRow));
startRow += splitRow;
}
//呼叫不同執行緒上獨立執行的任務
invokeAll(myTaskList);
List<Integer> ret = new ArrayList<>();
//歸併
for (MyTask myTask : myTaskList) {
ret.addAll(myTask.join());
}
return ret;
}
}
}
處理結果:
通過上文展示的方法,大家可以在不加鎖的方式來增加任務處理的效率,遇到類似於爬蟲資料處理、資料遷移等場景都可以採用,實測效果還不錯。當然,根據處理結果來分析,CompletionService的效率大概更高一些~。
大家好,我是練習java兩年半時間的南橘,下面是我的微信,需要之前的導圖或者想互相交流經驗的小夥伴可以一起互相交流哦。