【進階之路】多執行緒條件下分段處理List集合的幾種方法

南橘ryc發表於2021-06-04

這兩個月來因為工作和家庭的事情,導致一直都很忙,沒有多少時間去汲取養分,也就沒有什麼產出,最近稍微輕鬆了一點,後續的【進階之路】會慢慢回到正軌。

開門見山的說,第一次接觸到多執行緒處理同一個任務,是使用IO多執行緒下載檔案,之後也一直沒有再處理這一塊的任務,直到前幾天有同事問我,為什麼多執行緒處理一個list集合會出現各種bug,以及如何使用多執行緒的方式處理同一個list集合。

第一、為什麼會出現類似於重複處理某一個模組的問題?

我們都知道,在Java中,每個執行緒都有自己獨立的工作記憶體,執行緒對共享變數的所有操作都必須在自己的工作記憶體中進行,不能直接從主記憶體中讀寫。

如果執行緒1的修改內容想被執行緒2得到,那麼執行緒1工作記憶體中修改後的共享變數需要先重新整理到主記憶體中,再把主記憶體中更新過的共享變數更新到工作記憶體2中。

image.png

這個時候一般我們是考慮使用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);
    }
}

處理結果:

image.png

從結果上來看,還是比較美好的,通過CompletionService能夠比較快速地分段處理任務,我之前也有提過,合理的執行緒池大小設計有助於提高任務的處理效率,網上通用的設定方法一般是這樣的:

最佳執行緒數目 = ((執行緒等待時間+執行緒CPU時間)/執行緒CPU時間 )* CPU數目

進而得出

最佳執行緒數目 = (執行緒等待時間與執行緒CPU時間之比 + 1)* CPU數目

二、ForkJoinPool

當然,除了使用CompletionService之外,也可以使用ForkJoinPool來設計一個處理方法。

image.png

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

處理結果:

image.png

通過上文展示的方法,大家可以在不加鎖的方式來增加任務處理的效率,遇到類似於爬蟲資料處理、資料遷移等場景都可以採用,實測效果還不錯。當然,根據處理結果來分析,CompletionService的效率大概更高一些~。

大家好,我是練習java兩年半時間的南橘,下面是我的微信,需要之前的導圖或者想互相交流經驗的小夥伴可以一起互相交流哦。

相關文章