關於在項目中寫批量導入更新功能的一些總結

使用多線程

常見的四種線程池

newCachedThreadPool創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閒線程,若無可回收,則新建線程。

private static void excuteWay() {
        //可緩存線程池,先查看池中有沒有以前建立的線程,如果有,就直接使用。
        // 如果沒有,就建一個新的線程加入池中,緩存型池子通常用於執行一些生存期很短的異步型任務
        ExecutorService executorService = Executors.newCachedThreadPool();


    }

newFixedThreadPool 創建一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待。

private static void excuteWay2() {
        // 創建一個可重用固定個數的線程池,以共享的無界隊列方式來運行這些線程。
        ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

        try {
            for (int i = 0; i < 8; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("循環:"+i);
                executorService.execute(() -> {
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName());
                });
            }
            System.out.println("循環結束");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            executorService.shutdown();
            System.out.println("結束");
        }

        System.out.println(executorService.isShutdown());
        System.out.println(executorService.isTerminated());
        while(true){
            if(executorService.isTerminated()){
                System.out.println("所有的子線程都結束了!");
                System.out.println(executorService.isTerminated());
                break;
            }
            System.out.println(executorService.isTerminated());
        }
        /*// 關閉線程池
        executorService.shutdown();
        while (!executorService.isTerminated()) {//等待線程池中的線程執行完
            System.out.println(list.size());
            for (int i = 0; i < 10; i++) {
                System.out.println(list.get(i));
            }
        }*/
    }

newScheduledThreadPool 創建一個定長線程池,支持定時及週期性任務執行。

private static void excuteWay3() {
        //創建一個定長線程池,支持定時及週期性任務執行——延遲執行
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
        scheduledExecutorService.schedule(() ->
                System.out.println(Thread.currentThread().getName() + "newScheduledThreadPool"), 3, TimeUnit.SECONDS);

        // period 連續執行之間的時間間隔
        scheduledExecutorService.scheduleAtFixedRate(() ->
                System.out.println(Thread.currentThread().getName() + "newScheduledThreadPool2"), 3, 5, TimeUnit.SECONDS);

        // delay 從終止執行到開始執行之間的延遲
        scheduledExecutorService.scheduleWithFixedDelay(() ->
                System.out.println(Thread.currentThread().getName() + "newScheduledThreadPool3"), 3, 5, TimeUnit.SECONDS);
    }

newSingleThreadExecutor 創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。

private static void excuteWay4() {
        // 創建一個單線程化的線程池
        // 它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            singleThreadExecutor.execute(() -> System.out.println(Thread.currentThread().getName() + " i:" + finalI));
        }

    }

使用newFixedThreadPool多線程異步處理

模擬普通處理
public static void main(String[] args) {
        List<User2> user2List = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            User2 user2 = new User2();
            user2.setUserId((long) i);
            user2.setUserName("張" + i);
            user2List.add(user2);
        }
        System.out.println(user2List.size());

        Long t1= System.currentTimeMillis();
        System.out.println(t1);
        opt1(user2List);

//        opt2(user2List);
        Long t2= System.currentTimeMillis();
        System.out.println(t2);
        System.out.println("總耗時:" + (t2-t1));

//        excuteWay();
//        excuteWay2();
//        excuteWay3();
//        excuteWay4();
    }
    
    private static void opt1(List<User2> user2List) {
        user2List.forEach(TaskDemo::opt);
    }
    
    /**
     * 模擬操作 一條數據定0.5s
     * @param user2
     */
    private static void opt(User2 user2) {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
日誌輸出:
1000
1570694095654
1570694596373
總耗時:500719

Process finished with exit code 0
多線程異步處理
private static void opt2(List<User2> user2List) {
        long t1 = System.currentTimeMillis();
        System.out.println("裏面的t1:" + t1);
        int count = 100; //一個線程處理100條數據
        int listSize = user2List.size(); //總條數
        int runSize = (listSize / count) + 1;  //開啓的線程數

        ExecutorService executorService = Executors.newFixedThreadPool(runSize);
        try {
            List<User2> newList; //存放每個線程的執行數據
            for (int i = 0; i < runSize; i++) {
                //計算每個線程執行的數據
                if ((i + 1) == runSize) {
                    int startIndex = (i * count);
                    newList = user2List.subList(startIndex, listSize);
                } else {
                    int startIndex = (i * count);
                    int endIndex = (i + 1) * count;
                    newList = user2List.subList(startIndex, endIndex);
                }
                if (newList.size() > 0) {
                    System.out.println(newList.get(0).getUserName() + "開始被處理");
                    List<User2> finalNewList = newList;
                    executorService.execute(() -> {
                        // 異步執行
                        opt1(finalNewList);
                    });
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            executorService.shutdown();
            while(true){
                if(executorService.isTerminated()){
                    System.out.println("所有的子線程都結束了!");
                    System.out.println(executorService.isTerminated());
                    long t2 = System.currentTimeMillis();
                    System.out.println("裏面的t2:" + t2);
                    break;
                }
            }
        }
    }
日誌輸出:
1000
1570699575997
裏面的t1:1570699575997
張0開始被處理
張100開始被處理
張200開始被處理
張300開始被處理
張400開始被處理
張500開始被處理
張600開始被處理
張700開始被處理
張800開始被處理
張900開始被處理
所有的子線程都結束了!
true
裏面的t2:1570699626128
1570699626128
總耗時:50131

Process finished with exit code 0
    

由於是異步處理,所以for循環會迅速過去,所以在finally裏面使用while true去計算所有子線程執行時間,通過結果可以看到在總耗時上500719大概就是50131的10倍

注意在finally中關閉線程池

關於shutdown、shutdownNow、isTerminated、isShutDown:
shutdown只是將線程池的狀態設置爲SHUTWDOWN狀態,正在執行的任務會繼續執行下去,沒有被執行的則中斷。而shutdownNow則是將線程池的狀態設置爲STOP,正在執行的任務則被停止,沒被執行任務的則返回。

isShutDown當調用shutdown()或shutdownNow()方法後返回爲true。
isTerminated當調用shutdown()方法後,並且所有提交的任務完成後返回爲true;

isTerminated當調用shutdownNow()方法後,成功停止後返回爲true;

注意除非首先調用shutdown或shutdownNow,否則isTerminated永不爲true。。

使用java8新特性的並行流parallelStream

private static void opt3(List<User2> user2List) {
        user2List.parallelStream().forEach(TaskDemo::opt);
    }
    
日誌輸出:
1000
1570701685792
1570701748918
總耗時:63126

Process finished with exit code 0

可以看到,使用並行流後,效率相比普通方法快了許多倍,接近開10個線程的效率了


瞭解parallelStream的底層實現,默認使用了fork-join框架,其默認線程數是CPU核心數,而我的筆記本是四核8線程,正好符合耗時上的比例:

可以使用Runtime.availableProcessors()得到默認線程數

500719 ÷ 63126 = 7.932

於是我們可以想到通過調大ForkJoinPool線程池的數量從而繼續提高效率

通過了解知道了三種方式:

  1. 通過虛擬機啓動參數:
    -Djava.util.concurrent.ForkJoinPool.common.parallelism=N
  2. 在運行代碼之前,加入如下代碼:
    System.setProperty(“java.util.concurrent.ForkJoinPool.common.parallelism”, “N”);
  3. 自定義ForkJoinPool線程池數量:
    ForkJoinPool myPool = new ForkJoinPool(N);
private static void opt4(List<User2> user2List) {
        ForkJoinPool myPool = new ForkJoinPool(16);
        try {
            myPool.execute(() -> {
                user2List.parallelStream().forEach(TaskDemo::opt);
            });
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 返回從其它線程任務隊列中竊取的任務數估計值。
            System.out.println(myPool.getStealCount()); 
            myPool.shutdown();
            while(true){
                if(myPool.isTerminated()){
                    System.out.println("所有的子線程都結束了!");
                    break;
                }
            }
        }
    }

輸出日誌:
1000
1570709254777
0
所有的子線程都結束了!
1570709286381
總耗時:31604

Process finished with exit code 0

自定義線程池增大數量,效率又快了許多

500719 / 31604 = 15.85 接近16

我本地測試並不是ForkJoinPool的數量越多,效率就越高,在數量達到某個上限值後,耗時就固定在一個數值左右了,在這個數量之前,數量倒是和耗時趨近一個反比的線性關係


關於ForkJoinPool的一些詳解可以網上看看,我也看了一些

比如:https://blog.csdn.net/huanghanqian/article/details/81837362

比如ForkJoinPool的api翻譯:
https://blog.csdn.net/coffeelifelau/article/details/53908072

其他總結
  • invoke、execute和submit
    使用ForkJoinPool的時候發現執行任務的方法有:

    invoke(ForkJoinTask task)
    execute(ForkJoinTask<?> task)
    submit(ForkJoinTask task)
    
    由於不是很熟悉,因此做下筆記
    首先比較execute和submit的區別,觀察源碼發現:

    /**
     * Arranges for (asynchronous) execution of the given task.
     *
     * @param task the task
     * @throws NullPointerException if the task is null
     * @throws RejectedExecutionException if the task cannot be
     *         scheduled for execution
     */
    public void execute(ForkJoinTask<?> task) {
        if (task == null)
            throw new NullPointerException();
        externalPush(task);
    }

    /**
     * Submits a ForkJoinTask for execution.
     *
     * @param task the task to submit
     * @param <T> the type of the task's result
     * @return the task
     * @throws NullPointerException if the task is null
     * @throws RejectedExecutionException if the task cannot be
     *         scheduled for execution
     */
    public <T> ForkJoinTask<T> submit(ForkJoinTask<T> task) {
        if (task == null)
            throw new NullPointerException();
        externalPush(task);
        return task;
    }

    從方法體和返回參數可以看出,兩者邏輯大致相同,都是首先對任務做非空校驗,再將任務壓入執行隊列,
    唯一的不同是submit會把任務對象本身返回,返回後我們可以通過get()獲取方法執行結果。

    再看invoke()

    /**
     * Performs the given task, returning its result upon completion.
     * If the computation encounters an unchecked Exception or Error,
     * it is rethrown as the outcome of this invocation.  Rethrown
     * exceptions behave in the same way as regular exceptions, but,
     * when possible, contain stack traces (as displayed for example
     * using {@code ex.printStackTrace()}) of both the current thread
     * as well as the thread actually encountering the exception;
     * minimally only the latter.
     *
     * @param task the task
     * @param <T> the type of the task's result
     * @return the task's result
     * @throws NullPointerException if the task is null
     * @throws RejectedExecutionException if the task cannot be
     *         scheduled for execution
     */
    public <T> T invoke(ForkJoinTask<T> task) {
        if (task == null)
            throw new NullPointerException();
        externalPush(task);
        return task.join();
    }

    和submit()不同的是返回task.join(),再看join()方法

    /**
     * Returns the result of the computation when it {@link #isDone is
     * done}.  This method differs from {@link #get()} in that
     * abnormal completion results in {@code RuntimeException} or
     * {@code Error}, not {@code ExecutionException}, and that
     * interrupts of the calling thread do <em>not</em> cause the
     * method to abruptly return by throwing {@code
     * InterruptedException}.
     *
     * @return the computed result
     */
    public final V join() {
        int s;
        if ((s = doJoin() & DONE_MASK) != NORMAL)
            reportException(s);
        return getRawResult();
    }

    首先判斷任務是否執行完畢,若未執行完畢,拋出任務取消或者任務異常的異常Exception,否則獲取任務執行結果。
    而getRawResult()就是取任務的結果返回。這裏看判斷任務執行完畢的doJoin()方法

    /**
     * Implementation for join, get, quietlyJoin. Directly handles
     * only cases of already-completed, external wait, and
     * unfork+exec.  Others are relayed to ForkJoinPool.awaitJoin.
     *
     * @return status upon completion
     */
    private int doJoin() {
        int s; Thread t; ForkJoinWorkerThread wt; ForkJoinPool.WorkQueue w;
        return (s = status) < 0 ? s :
            ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ?
            (w = (wt = (ForkJoinWorkerThread)t).workQueue).
            tryUnpush(this) && (s = doExec()) < 0 ? s :
            wt.pool.awaitJoin(w, this, 0L) :
            externalAwaitDone();
    }

    這裏用了多個三元運算符,整理下邏輯
    
    判斷當前任務的執行狀態
    當前狀態不是已完成
    當前線程是ForkJoin線程
    從工作隊列中取出一個任務執行
    否則等待當前任務執行完畢
    所以invoke()方法的調用,會一直阻塞到任務執行完成返回
    

總結一下:

execute(ForkJoinTask) 異步執行tasks,無返回值
invoke(ForkJoinTask) 有Join, tasks會被同步到主進程
submit(ForkJoinTask) 異步執行,且帶Task返回值,可通過task.get 實現同步到主線程
  • 關於線程安全問題
    在並行流的循環裏面一定要注意線程安全的問題
    比如ArrayList、HashMap非線程安全的
    
    考慮原子性問題
    比如list、map的add方法非原子性的
    
    所有我們在這中間使用鏈表時,需使用線程安全的或者讓其線程安全
    
    比如
    使ArrayList線程安全:
    網上有三種方法,這裏記錄一種
    List<User2> list = Collections.synchronizedList(new ArrayList<>());
    
    使HashMap線程安全:
    Map map = new ConcurrentHashMap();
    Map map2 = Collections.synchronizedMap(new HashMap<>());
    
    其他,比如一些原子類的使用
    AtomicLong i = new AtomicLong(0);
    i.incrementAndGet();
    等等

在這裏插入圖片描述

其他關於批量導入更新的總結
儘可能一次性通過sql語句用構建臨時表對象去關聯需要導入表數據的關聯表,得到關聯表中的字段做校驗或者數據導入,
而不是查一個關聯字段就和數據庫進行一次交互,減少和數據庫的交互。

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章