文章目錄
使用多線程
常見的四種線程池
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線程池的數量從而繼續提高效率
通過了解知道了三種方式:
- 通過虛擬機啓動參數:
-Djava.util.concurrent.ForkJoinPool.common.parallelism=N - 在運行代碼之前,加入如下代碼:
System.setProperty(“java.util.concurrent.ForkJoinPool.common.parallelism”, “N”); - 自定義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語句用構建臨時表對象去關聯需要導入表數據的關聯表,得到關聯表中的字段做校驗或者數據導入,
而不是查一個關聯字段就和數據庫進行一次交互,減少和數據庫的交互。