併發工具
一、線程池
線程池是指管理一組同構工作線程的線程的資源池。
線程池與**工作隊列(Work Queue)模切相關,工作隊列中保存了所有等待知心的任務。線程池中的工作線程(Work Thread)**的任務很簡單:從工作隊列中獲取一個任務,執行任務,然後返回線程池並等待下一個任務。
使用線程池的好處是:
- 通過重用現有的線程而不是創建新線程,可以在處理多個請求時分攤在線程創建和銷燬過程中產生的巨大開銷。
- 當請求到達時,工作線程通常已經存在,因此不會由於等待創建工作線程而延遲任務的執行,從而提高響應性
- 通過適當調節線程池的大小,可以創建足夠多的線程以便使處理器保持忙碌狀態,同時還可以防止創建過多線程相互競爭資源使應用耗盡內存或失敗。
1、自定義線程池
步驟1:自定義拒絕策略接口
//如果隊列已滿時的拒絕策略接口
@FunctionalInterface
interface RejectPolicy<T> {
void reject(BlockingQueue<T> blockingQueue, T task);
}
步驟2:自定義任務隊列
//工作隊列
class BlockingQueue<T> {
//任務列表
private Deque<T> queue = new ArrayDeque<>();
//鎖
private ReentrantLock lock = new ReentrantLock();
//生產者條件變量
private Condition fullWaitSet = lock.newCondition();
//消費者條件變量
private Condition emptyWaitSet = lock.newCondition();
//容量
private int capacity;
public BlockingQueue(int capacity) {
this.capacity = capacity;
}
//阻塞獲取
public T take() {
lock.lock();
try {
//隊列爲空時等待
while (queue.isEmpty()) {
try {
emptyWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//不爲空時,從隊列中獲取
T t = queue.removeFirst();
fullWaitSet.signalAll();
return t;
} finally {
lock.unlock();
}
}
//帶超時的阻塞獲取
public T take(long timeout, TimeUnit unit) {
lock.lock();
try {
//剩餘等待時間(納秒)
long nanos = unit.toNanos(timeout);
while (queue.isEmpty()) {
try {
if (nanos <= 0) {
return null;
}
//awaitNanos返回剩餘等待時間,防止虛假喚醒再次等待timeout時間
nanos = emptyWaitSet.awaitNanos(nanos);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
T t = queue.removeFirst();
fullWaitSet.signalAll();
return t;
} finally {
lock.unlock();
}
}
//阻塞添加
public void put(T t) {
lock.lock();
try {
//隊列滿時等待進入
while (queue.size() >= capacity) {
try {
System.out.println("等待加入任務隊列" + t);
fullWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//隊列不滿時直接進入任務隊列
System.out.println("加入任務隊列" + t);
queue.addLast(t);
emptyWaitSet.signalAll();
} finally {
lock.unlock();
}
}
//帶超時時間的阻塞添加
public boolean put(T t, long timeout, TimeUnit timeUnit) {
lock.lock();
try {
//等待進入任務隊列的超時時間
long nanos = timeUnit.toNanos(timeout);
while (queue.size() >= capacity) {
try {
System.out.println("等待加入任務隊列" + t);
if (nanos <= 0) {
return false;
}
nanos = fullWaitSet.awaitNanos(nanos);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("加入任務隊列" + t);
queue.addLast(t);
emptyWaitSet.signalAll();
return true;
} finally {
lock.unlock();
}
}
//獲取隊列大小
public int size() {
lock.lock();
try {
return queue.size();
} finally {
lock.unlock();
}
}
//執行用戶傳入的拒絕策略
public void tryPut(RejectPolicy<T> rejectPolicy, T task) {
lock.lock();
try {
//判斷隊列是否已滿
if (queue.size() >= capacity) {
//執行用戶傳入的拒絕策略
rejectPolicy.reject(this, task);
} else { //有空閒,直接加入任務隊列
System.out.println("加入任務隊列 " + task);
queue.add(task);
emptyWaitSet.signalAll();
}
} finally {
lock.unlock();
}
}
}
步驟3:自定義線程池
//自定義線程池
class ThreadPool {
//工作隊列
private BlockingQueue<Runnable> taskQueue;
//線程集合
private HashSet<Worker> workers = new HashSet<>();
//核心線程的數目
private int coreSize;
//獲得任務的超時時間
private long timeout;
//拒絕策略
private RejectPolicy<Runnable> rejectPolicy;
//時間單位
private TimeUnit timeUnit;
public ThreadPool(int coreSize, long timeout, TimeUnit timeUnit, int taskCapacity, RejectPolicy<Runnable> rejectPolicy) {
this.coreSize = coreSize;
this.timeout = timeout;
this.timeUnit = timeUnit;
this.rejectPolicy = rejectPolicy;
taskQueue = new BlockingQueue<>(taskCapacity);
}
public void execute(Runnable task) {
synchronized (workers) {
//當任務數沒有超過coreSize,則直接創建worker並將任務交給其執行
if (workers.size() < coreSize) {
Worker worker = new Worker(task);
System.out.println("新增worker" + worker + " " + task);
workers.add(worker);
worker.start();
} else {
//如果任務數超過coreSize,則將其放入任務隊列等待
// taskQueue.put(task);
/**
* 如果隊列已滿時的策略
* 1、死等
* 2、帶超時的等待
* 3、讓調用者放棄執行任務
* 4、讓調用者拋出異常
* 5、讓調用者自己執行任務
*/
taskQueue.tryPut(rejectPolicy, task);
}
}
}
//工作線程類
class Worker extends Thread {
private Runnable task;
public Worker(Runnable task) {
this.task = task;
}
//執行任務
@Override
public void run() {
//1.如果task不爲空,則執行task
//2.當task執行完畢,要從任務隊列中獲取任務並執行
while (task != null || (task = taskQueue.take(timeout, timeUnit)) != null) {
try {
System.out.println(Thread.currentThread() + " 正在執行" + task);
task.run();
} catch (Exception e) {
e.printStackTrace();
} finally {
//task執行完之後,從任務隊列中獲取任務
task = null;
}
}
//當前任務都執行完畢之後,將自身從隊列中移除
synchronized (workers) {
System.out.println(Thread.currentThread() + " 執行完畢被移除");
workers.remove(this);
}
}
}
}
步驟4:測試
public static void main(String[] args) {
ThreadPool threadPool = new ThreadPool(
1, 1000, TimeUnit.MILLISECONDS, 1, (queue, task) -> {
//1、死等
queue.put(task);
//2、帶超時的等待
// boolean put = queue.put(task, 2000, TimeUnit.MILLISECONDS);
// System.out.println("等待執行 " + (put ? "成功" : "失敗"));
//3、讓調用者放棄執行任務
// System.out.println("放棄執行任務 " + task);
//4、讓調用者拋出異常
// throw new RuntimeException("任務執行失敗:" + task);
//5、讓調用者自己執行任務
// task.run();
});
//使用自定義線程池執行線程
for (int i = 0; i < 3; i++) {
int finalI = i;
threadPool.execute(() -> {
System.out.println("task " + finalI + " runing....");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
2、 ThreadPoolExecutor
ThreadPoolExecutor類的繼承關係:
1)線程池狀態
ThreadPoolExecutor 使用 int 的高 3 位來表示線程池狀態,低 29 位表示線程數量
狀態名 | 高3位 | 接受新任務 | 處理阻塞隊列任務 | 說明 |
---|---|---|---|---|
RUNNING | 111 | Y | Y | — |
SHUTDOWN | 000 | N | Y | 不會接收新任務,但會處理阻塞隊列剩餘 任務 |
STOP | 001 | N | N | 會中斷正在執行的任務,並拋棄阻塞隊列 任務 |
TIDYING | 010 | — | — | 任務全執行完畢,活動線程爲 0 即將進入 終結 |
TERMINATED | 011 | — | — | 終結狀態 |
從數字大小上比較,TERMINATED > TIDYING > STOP > SHUTDOWN > RUNNING
這些信息存儲在一個原子變量 ctl 中,目的是將線程池狀態與線程個數合二爲一,這樣就可以用一次 cas 原子操作 進行賦值
// c 爲舊值, ctlOf 返回結果爲新值
ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))));
// rs 爲高 3 位代表線程池狀態, wc 爲低 29 位代表線程個數,ctl 使用位或合併它們
private static int ctlOf(int rs, int wc) {
return rs | wc;
}
2) 構造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
參數介紹:
- corePoolSize 核心線程數目 (多保留的線程數)
- maximumPoolSize 大線程數目 (核心線程+救濟線程)
- keepAliveTime 生存時間 - 針對救急線程
- unit 時間單位 - 針對救急線程
- workQueue 阻塞隊列
- threadFactory 線程工廠 - 可以爲線程創建時起個好名字
- handler 拒絕策略
當任務阻塞隊列滿時,線程池不會直接調用 拒絕策略 ,而是創建 救濟線程 對超出的任務進行執行。執行完畢救濟線程就會進入終結狀態。如果當前救濟線程也全部被使用,那麼纔會調用拒絕策略。
工作方式:
- 線程池中剛開始沒有線程,當一個任務提交給線程池之後,線程池會創建一個新的線程執行任務
- 當線程數達到corePoolSize 並且當前沒有空閒的線程,這時再加入任務,新加的任務會被加入到workQueue 隊列排隊阻塞,直到有空閒的線程
- 如果隊列選擇了有界隊列,那麼任務超過了隊列大小時,會創建
maximumPoolSize - corePoolSize
數目的救濟線程救急 - 如果線程數到達maximumPoolSize 仍然有新任務,這時就會執行拒絕策略,拒絕策略 jdk 提供了 4 種實現
- AbortPolicy 讓調用者拋出 RejectedExecutionException 異常,這是默認策略
- CallerRunsPolicy 讓調用者自己運行任務
- DiscardPolicy 放棄本次任務
- DiscardOldestPolicy 放棄隊列中早的任務,本任務取而代之
- 除了上面的四種,,其它 著名框架也提供了實現
- Dubbo 的實現,在拋出 RejectedExecutionException 異常之前會記錄日誌,並 dump 線程棧信息,方 便定位問題
- Netty 的實現,是創建一個新線程來執行任務
- ActiveMQ 的實現,帶超時等待(60s)嘗試放入隊列,類似我們之前自定義的拒絕策略
- PinPoint 的實現,它使用了一個拒絕策略鏈,會逐一嘗試策略鏈中每種拒絕策略
- 當高峯過去後,超過corePoolSize 的救急線程如果一段時間沒有任務做,需要結束節省資源進行終止,這個時間由 keepAliveTime 和 unit 來控制。
根據這個構造方法,JDK Executors 類中提供了衆多工廠方法來創建各種用途的線程池
3) newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
特點:
- 核心線程數 == 大線程數(沒有救急線程被創建),因此也無需超時時間
- 阻塞隊列是無界的,可以放任意數量的任務
- 適用於任務量已知,相對耗時的任務
使用:
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2, new ThreadFactory() {
//ThreadFactory用於給線程起名字
private AtomicInteger t = new AtomicInteger(1);
@Override
public Thread newThread(@NotNull Runnable r) {
return new Thread(r,"我的線程"+t.getAndIncrement());
}
});
executorService.execute(()->{
TheadPrint.print("1");
});
executorService.execute(()->{
TheadPrint.print("2");
});
executorService.execute(()->{
TheadPrint.print("3");
});
}
結果:
12:33:17:038 Thread[我的線程2,5,main]: 2
12:33:17:040 Thread[我的線程2,5,main]: 3
12:33:17:047 Thread[我的線程1,5,main]: 1
其中ThreadFactory主要用來更改線程的名字,可以省略
4) newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
特點:
- 核心線程數爲0,最大線程數爲
Integer.MAX_VALUE
,救急線程的空閒生存時間是 60s,意味着- 全部都是救急線程(60s 後可以回收)
- 救急線程可以無限創建
- 隊列採用了 SynchronousQueue 實現特點是,它沒有容量,沒有線程來取是放不進去的(一手交錢、一手交 貨),從下面例子可以看出在t3執行take操作之前,t2無法把2放入隊列
public static void main(String[] args) throws InterruptedException {
SynchronousQueue<Integer> integers = new SynchronousQueue<>();
new Thread(() -> {
try {
TheadPrint.print("putting... "+ 1);
integers.put(1);
TheadPrint.print("putted..." + 1);
TheadPrint.print("putting... "+ 2);
integers.put(2);
TheadPrint.print("putted..." + 2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1").start();
Thread.sleep(1000);
new Thread(() -> {
try {
TheadPrint.print("taking " + 1);
integers.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t2").start();
Thread.sleep(1000);
new Thread(() -> {
try {
TheadPrint.print("taking " + 2);
integers.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t3").start();
}
結果:
12:52:41:123 Thread[t1,5,main]: putting... 1
12:52:42:037 Thread[t2,5,main]: taking 1
12:52:42:042 Thread[t1,5,main]: putted...1
12:52:42:042 Thread[t1,5,main]: putting... 2
12:52:43:038 Thread[t3,5,main]: taking 2
12:52:43:038 Thread[t1,5,main]: putted...2
- 整個線程池表現爲線程數會根據任務量不斷增長,沒有上限,當任務執行完畢,空閒 1分鐘後釋放線程。 適合任務數比較密集,但每個任務執行時間較短的情況
5)newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
使用場景:
希望多個任務排隊執行。線程數固定爲 1,任務數多於 1 時,會放入無界隊列排隊。任務執行完畢,這唯一的線程 也不會被釋放。
區別:
- 自己創建一個單線程串行執行任務,如果任務執行失敗而終止那麼沒有任何補救措施,而線程池還會新建一 個線程,保證池的正常工作
- Executors.newSingleThreadExecutor() 線程個數始終爲1,不能修改
- FinalizableDelegatedExecutorService 應用的是裝飾器模式,只對外暴露了 ExecutorService 接口,因 此不能調用 ThreadPoolExecutor 中特有的方法
- Executors.newFixedThreadPool(1) 初始時爲1,以後還可以修改
- 對外暴露的是 ThreadPoolExecutor 對象,可以強轉後調用 setCorePoolSize 等方法進行修改
3、提交任務
1)execute
// 執行任務
void execute(Runnable command);
2)submit
帶返回結果的調用,返回結果使用Future接收(使用了保護暫停模式)
// 提交任務 task,用返回值 Future 獲得任務執行結果
<T> Future<T> submit(Callable<T> task);
使用示例:
ExecutorService executorService = Executors.newFixedThreadPool(2);
Future<String> future = executorService.submit(new Callable<String>() {
@Override
public String call() throws Exception {
TheadPrint.print("run...");
Thread.sleep(1000);
return "Hello World!";
}
});
TheadPrint.print(future.get());
結果:
13:22:54:092 Thread[pool-1-thread-1,5,main]: run...
13:22:55:094 Thread[main,5,main]: Hello World!
3)invokeAll
帶返回結果的調用,並且可以返回多個結果
// 提交 tasks 中所有任務
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException;
// 提交 tasks 中所有任務,帶超時時間
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException;
示例:
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(2);
List<Future<String>> futureList = executorService.invokeAll(Arrays.asList(
() -> {
TheadPrint.print("1");
Thread.sleep(1000);
return "1";
},
() -> {
TheadPrint.print("2");
Thread.sleep(1000);
return "2";
},
() -> {
TheadPrint.print("3");
Thread.sleep(1000);
return "3";
}
));
futureList.forEach((f) -> {
try {
TheadPrint.print(f.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
});
}
結果:
15:01:06:860 Thread[pool-1-thread-2,5,main]: 2
15:01:06:860 Thread[pool-1-thread-1,5,main]: 1
15:01:07:862 Thread[pool-1-thread-2,5,main]: 3
15:01:08:872 Thread[main,5,main]: 1
15:01:08:872 Thread[main,5,main]: 2
15:01:08:873 Thread[main,5,main]: 3
4)invokeAny
帶返回結果的調用,只返回最先執行完畢的那一個線程的結果
// 提交 tasks 中所有任務,哪個任務先成功執行完畢,返回此任務執行結果,其它任務取消
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException;
// 提交 tasks 中所有任務,哪個任務先成功執行完畢,返回此任務執行結果,其它任務取消,帶超時時間
<T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
示例:
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(1);
String str = executorService.invokeAny(Arrays.asList(
() -> {
TheadPrint.print("1");
Thread.sleep(2000);
TheadPrint.print("1");
return "1";
},
() -> {
TheadPrint.print("2");
Thread.sleep(2000);
TheadPrint.print("2");
return "2";
},
() -> {
TheadPrint.print("3");
Thread.sleep(2000);
TheadPrint.print("3");
return "3";
}
));
TheadPrint.print(str);
}
結果:
15:12:48:309 Thread[pool-1-thread-1,5,main]: 1
15:12:50:319 Thread[pool-1-thread-1,5,main]: 1
15:12:50:320 Thread[pool-1-thread-1,5,main]: 2
15:12:50:321 Thread[main,5,main]: 1
4、關閉線程池
1)shutdown
/*
線程池狀態變爲 SHUTDOWN
- 不會接收新任務
- 但已提交任務會執行完,包括阻塞隊列中的任務
- 此方法不會阻塞調用線程的執行
*/
void shutdown();
具體實現:
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
// 修改線程池狀態
advanceRunState(SHUTDOWN);
// 僅會打斷空閒線程
interruptIdleWorkers();
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
// 嘗試終結(沒有運行的線程可以立刻終結,如果還有運行的線程也不會等)
tryTerminate();
}
使用示例:
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.submit(
() -> {
TheadPrint.print("begin 1....");
Thread.sleep(1000);
TheadPrint.print("end 1....");
return "1";
}
);
executorService.submit(
() -> {
TheadPrint.print("begin 2....");
Thread.sleep(1000);
TheadPrint.print("end 2....");
return "1";
}
);
executorService.submit(
() -> {
TheadPrint.print("begin 3....");
Thread.sleep(1000);
TheadPrint.print("end 3....");
return "1";
}
);
TheadPrint.print("shutdown...");
executorService.shutdown();
}
下面是程序的運行結果,可以看出所有任務都執行完畢了,雖然任務3會被放入阻塞隊列,但是仍然會被執行
15:58:57:687 Thread[pool-1-thread-1,5,main]: begin 1....
15:58:57:750 Thread[pool-1-thread-2,5,main]: begin 2....
15:58:57:756 Thread[main,5,main]: shutdown...
15:58:58:691 Thread[pool-1-thread-1,5,main]: end 1....
15:58:58:694 Thread[pool-1-thread-1,5,main]: begin 3....
15:58:58:753 Thread[pool-1-thread-2,5,main]: end 2....
15:58:59:719 Thread[pool-1-thread-1,5,main]: end 3....
如果在shutdown之後再調用任務執行,就會拋出異常:
TheadPrint.print("shutdown...");
executorService.shutdown();
executorService.submit(
() -> {
TheadPrint.print("begin 4....");
Thread.sleep(1000);
TheadPrint.print("end 4....");
return "1";
}
);
結果
16:04:27:232 Thread[pool-1-thread-2,5,main]: begin 2....
16:04:27:237 Thread[main,5,main]: shutdown...
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@3f91beef rejected from java.util.concurrent.ThreadPoolExecutor@1a6c5a9e[Shutting down, pool size = 2, active threads = 2, queued tasks = 1, completed tasks = 0]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:134)
at concurrent.ch8_tools.ThreadPoolExecutorTest.main(ThreadPoolExecutorTest.java:150)
16:04:27:255 Thread[pool-1-thread-1,5,main]: begin 1....
另外,調用showdown之後主線程會繼續運行,如果想等待任務執行結束需要調用下面的方法
//等待任務執行結束,而且最多等待10秒
executorService.awaitTermination(10, TimeUnit.SECONDS);
2)shutdownNow
/* 線程池狀態變爲 STOP
- 不會接收新任務
- 會將隊列中的任務返回
- 並用 interrupt 的方式中斷正在執行的任務
*/
List<Runnable> shutdownNow();
具體實現:
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
// 修改線程池狀態
advanceRunState(STOP);
// 打斷所有線程
interruptWorkers();
// 獲取隊列中剩餘任務
tasks = drainQueue();
} finally {
mainLock.unlock();
}
// 嘗試終結(一定能終結)
tryTerminate();
return tasks;
}
使用:
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.submit(
() -> {
TheadPrint.print("begin 1....");
Thread.sleep(1000);
TheadPrint.print("end 1....");
return "1";
}
);
executorService.submit(
() -> {
TheadPrint.print("begin 2....");
Thread.sleep(1000);
TheadPrint.print("end 2....");
return "1";
}
);
executorService.submit(
() -> {
TheadPrint.print("begin 3....");
Thread.sleep(1000);
TheadPrint.print("end 3....");
return "1";
}
);
TheadPrint.print("shutdownNow...");
List<Runnable> runnables = executorService.shutdownNow();
TheadPrint.print("沒有執行的任務個數:"+runnables.size());
}
輸出結果如下,從結果可以看出,任務1、2都只是剛開始而沒有被執行完畢。而在阻塞隊列中的任務3直接被返回了。
16:12:11:324 Thread[main,5,main]: shutdownNow...
16:12:11:326 Thread[main,5,main]: 沒有執行的任務個數:1
16:12:11:328 Thread[pool-1-thread-1,5,main]: begin 1....
16:12:11:325 Thread[pool-1-thread-2,5,main]: begin 2....
3)其他相關方法
// 不在 RUNNING 狀態的線程池,此方法就返回 true
boolean isShutdown();
// 線程池狀態是否是 TERMINATED
boolean isTerminated();
// 調用 shutdown 後,由於調用線程並不會等待所有任務運行結束,因此如果它想在線程池 TERMINATED 後做些事 情,可以利用此方法等待
boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
二、 設計模式之工作線程
1、定義
讓有限的工作線程(Worker Thread)來輪流異步處理無限多的任務。也可以將其歸類爲分工模式,它的典型實現 就是線程池,也體現了經典設計模式中的享元模式。
例如,海底撈的服務員(線程),輪流處理每位客人的點餐(任務),如果爲每位客人都配一名專屬的服務員,那 麼成本就太高了(對應另一種多線程設計模式:Thread-Per-Message)
注意,不同任務類型應該使用不同的線程池,這樣能夠避免飢餓,並能提升效率
2、飢餓
固定大小線程池會有飢餓現象
- 兩個工人是同一個線程池中的兩個線程
- 他們要做的事情是:爲客人點餐和到後廚做菜,這是兩個階段的工作
-
客人點餐:必須先點完餐,等菜做好,上菜,在此期間處理點餐的工人必須等待
-
後廚做菜:沒啥說的,做就是了
- 比如工人A 處理了點餐任務,接下來它要等着 工人B 把菜做好,然後上菜,他倆也配合的蠻好
- 但現在同時來了兩個客人,這個時候工人A 和工人B 都去處理點餐了,這時沒人做飯了,飢餓
-
示例代碼:
public class ThreadDeadLockTest {
static final List<String> MENU = Arrays.asList("地三鮮", "宮保雞丁", "辣子雞丁", "烤雞翅");
static Random RANDOM = new Random();
static String cooking() {
return MENU.get(RANDOM.nextInt(MENU.size()));
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.execute(() -> {
TheadPrint.print("處理點餐...");
Future<String> f = executorService.submit(() -> {
TheadPrint.print("做菜");
return cooking();
});
try {
TheadPrint.print("上菜: " + f.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
});
/*executorService.execute(() -> {
TheadPrint.print("處理點餐...");
Future<String> f = executorService.submit(() -> {
TheadPrint.print("做菜");
return cooking();
});
try {
TheadPrint.print("上菜: "+f.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
});*/
}
}
執行結果:
16:45:46:112 Thread[pool-1-thread-1,5,main]: 處理點餐...
16:45:46:126 Thread[pool-1-thread-2,5,main]: 做菜
16:45:46:126 Thread[pool-1-thread-1,5,main]: 上菜: 烤雞翅
當放開註釋之後,可能的結果如下,出現了飢餓。這是因爲兩個線程都在等待另外的線程進行做菜工作,但兩個線程都被佔用了,所以出現這種情況。
16:43:10:373 Thread[pool-1-thread-2,5,main]: 處理點餐...
16:43:10:382 Thread[pool-1-thread-1,5,main]: 處理點餐...
解決方法可以增加線程池的大小,不過不是根本解決方案,還是前面提到的,不同的任務類型,採用不同的線程 池,例如:
public class ThreadDeadLockTest {
static final List<String> MENU = Arrays.asList("地三鮮", "宮保雞丁", "辣子雞丁", "烤雞翅");
static Random RANDOM = new Random();
static String cooking() {
return MENU.get(RANDOM.nextInt(MENU.size()));
}
public static void main(String[] args) {
ExecutorService waiterPool = Executors.newFixedThreadPool(1);
ExecutorService cookerPool = Executors.newFixedThreadPool(1);
waiterPool.execute(() -> {
TheadPrint.print("處理點餐...");
Future<String> f = cookerPool.submit(() -> {
TheadPrint.print("做菜");
return cooking();
});
try {
TheadPrint.print("上菜: " + f.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
});
waiterPool.execute(() -> {
TheadPrint.print("處理點餐...");
Future<String> f = cookerPool.submit(() -> {
TheadPrint.print("做菜");
return cooking();
});
try {
TheadPrint.print("上菜: "+f.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
});
}
}
輸出如下,這樣就不會出現飢餓的問題:
16:48:14:594 Thread[pool-1-thread-1,5,main]: 處理點餐...
16:48:14:611 Thread[pool-2-thread-1,5,main]: 做菜
16:48:14:612 Thread[pool-1-thread-1,5,main]: 上菜: 烤雞翅
16:48:14:612 Thread[pool-1-thread-1,5,main]: 處理點餐...
16:48:14:613 Thread[pool-2-thread-1,5,main]: 做菜
16:48:14:614 Thread[pool-1-thread-1,5,main]: 上菜: 烤雞翅
3、創建多少線程池合適
- 過小會導致程序不能充分地利用系統資源、容易導致飢餓
- 過大會導致更多的線程上下文切換,佔用更多內存
CPU 密集型運算
通常採用cpu 核數 + 1
能夠實現最優的 CPU 利用率,+1 是保證當線程由於頁缺失故障(操作系統)或其它原因 導致暫停時,額外的這個線程就能頂上去,保證 CPU 時鐘週期不被浪費
I/O 密集型運算
CPU 不總是處於繁忙狀態,例如,當你執行業務計算時,這時候會使用 CPU 資源,但當你執行 I/O 操作時、遠程 RPC 調用時,包括進行數據庫操作時,這時候 CPU 就閒下來了,你可以利用多線程提高它的利用率。
經驗公式如下
線程數 = 核數 * 期望 CPU 利用率 * 總時間(CPU計算時間+等待時間) / CPU 計算時間
例如 4 核 CPU 計算時間是 50% ,其它等待時間是 50%,期望 cpu 被 100% 利用,套用公式
4 * 100% * 100% / 50% = 8
例如 4 核 CPU 計算時間是 10% ,其它等待時間是 90%,期望 cpu 被 100% 利用,套用公式
4 * 100% * 100% / 10% = 40
三、任務調度線程池
1、簡介
在『任務調度線程池』功能加入之前,可以使用 java.util.Timer 來實現定時功能,Timer 的優點在於簡單易用,但 由於所有任務都是由同一個線程來調度,因此所有任務都是串行執行的,同一時間只能有一個任務在執行,前一個 任務的延遲或異常都將會影響到之後的任務:
public static void main (String[]args){
Timer timer = new Timer();
TimerTask task1 = new TimerTask() {
@Override
public void run() {
log.debug("task 1");
sleep(2);
}
};
TimerTask task2 = new TimerTask() {
@Override
public void run() {
log.debug("task 2");
}
};
// 使用 timer 添加兩個任務,希望它們都在 1s 後執行
// 但由於 timer 內只有一個線程來順序執行隊列中的任務,因此『任務1』的延時,影響了『任務2』的執行
timer.schedule(task1, 1000);
timer.schedule(task2, 1000);
}
輸出如下所示,可以看出task2是在task1執行完畢之後執行的
20:46:09.444 c.TestTimer [main] - start...
20:46:10.447 c.TestTimer [Timer-0] - task 1
20:46:12.448 c.TestTimer [Timer-0] - task 2
使用 ScheduledExecutorService 改寫:
public static void main(String[] args) {
ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
pool.schedule(() -> {
TheadPrint.print("任務1...");
int i = 1 / 0;
}, 1, TimeUnit.SECONDS);
pool.schedule(() -> {
TheadPrint.print("任務2...");
}, 1, TimeUnit.SECONDS);
}
結果如下,可以看出兩個任務幾乎是同時執行的,而且任務1也沒有被一場打斷
18:21:19:360 Thread[pool-1-thread-1,5,main]: 任務1...
18:21:19:364 Thread[pool-1-thread-2,5,main]: 任務2...
整個線程池表現爲:線程數固定,任務數多於線程數時,會放入無界隊列排隊。任務執行完畢,這些線 程也不會被釋放。用來執行延遲或反覆執行的任務
2、週期執行
1)scheduleAtFixedRate函數
使用scheduleAtFixedRate函數可以實現週期執行:
ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
TheadPrint.print("開始執行任務");
pool.scheduleAtFixedRate(() -> {
TheadPrint.print("任務1...");
//開始時間,間隔時間,時間單位
}, 1, 1,TimeUnit.SECONDS);
結果如下所示,上面的代碼實現了一秒後開始執行,之後每隔一秒執行一次
18:53:56:407 Thread[main,5,main]: 開始執行任務
18:53:57:622 Thread[pool-1-thread-1,5,main]: 任務1...
18:53:58:621 Thread[pool-1-thread-1,5,main]: 任務1...
18:53:59:641 Thread[pool-1-thread-2,5,main]: 任務1...
需要注意的是,該函數如果任務執行時間超過間隔時間,那麼就不會繼續等待間隔時間,測試代碼如下:
ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
TheadPrint.print("開始執行任務");
pool.scheduleAtFixedRate(() -> {
TheadPrint.print("任務1...");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, 1, 1,TimeUnit.SECONDS);
可以看到每個任務執行時間的間隔爲3秒,而不是4秒,說明任務執行完之後並沒等待間隔時間
18:56:30:516 Thread[main,5,main]: 開始執行任務
18:56:31:690 Thread[pool-1-thread-1,5,main]: 任務1...
18:56:34:694 Thread[pool-1-thread-1,5,main]: 任務1...
18:56:37:695 Thread[pool-1-thread-1,5,main]: 任務1...
2)scheduleWithFixedDelay函數
和上面不同的是,scheduleWithFixedDelay函數,就算任務執行時間超過間隔時間,仍然會繼續等待間隔時間,測試代碼如下:
ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
TheadPrint.print("開始執行任務");
pool.scheduleWithFixedDelay(() -> {
TheadPrint.print("任務1...");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, 1, 1,TimeUnit.SECONDS);
結果如下,可以看出確實等待了4秒而不是3秒
19:01:24:926 Thread[main,5,main]: 開始執行任務
19:01:26:225 Thread[pool-1-thread-1,5,main]: 任務1...
19:01:30:230 Thread[pool-1-thread-1,5,main]: 任務1...
19:01:34:265 Thread[pool-1-thread-1,5,main]: 任務1...
3、異常處理
之前我們已經演示過了如果任務執行過程出現異常,ScheduledExecutorService中的其它任務不會被打斷。但是ScheduledExecutorService也沒有做任何處理,例如輸出控制檯。這時就需要我們自己對異常進行處理
1)主動捉異常
示例代碼:
ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
TheadPrint.print("開始執行任務");
pool.execute(()->{
TheadPrint.print("任務1...");
try {
int i = 1 / 0;
} catch (Exception e) {
e.printStackTrace();
}
});
pool.execute(()->{
TheadPrint.print("任務2...");
});
結果:
19:32:29:000 Thread[main,5,main]: 開始執行任務
19:32:29:705 Thread[pool-1-thread-1,5,main]: 任務1...
19:32:29:713 Thread[pool-1-thread-1,5,main]: 任務2...
java.lang.ArithmeticException: / by zero
at concurrent.ch8_tools.ThreadPoolExecutorTest.lambda$main$0(ThreadPoolExecutorTest.java:162)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
2)使用 Future
Future不僅能夠接收返回的結果,如果出現異常,那麼它接收的就是異常,示例代碼如下:
public static void main(String[] args) throws ExecutionException, InterruptedException {
ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
TheadPrint.print("開始執行任務");
Future<String> f = pool.submit(new Callable<String>() {
@Override
public String call() throws Exception {
TheadPrint.print("任務1...");
int i = 1 / 0;
return "成功";
}
});
TheadPrint.print(f.get());
}
結果:
19:38:09:757 Thread[main,5,main]: 開始執行任務
19:38:09:922 Thread[pool-1-thread-1,5,main]: 任務1...
Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.ArithmeticException: / by zero
at java.util.concurrent.FutureTask.report(FutureTask.java:122)
at java.util.concurrent.FutureTask.get(FutureTask.java:192)
at concurrent.ch8_tools.ThreadPoolExecutorTest.main(ThreadPoolExecutorTest.java:167)
Caused by: java.lang.ArithmeticException: / by zero
at concurrent.ch8_tools.ThreadPoolExecutorTest$1.call(ThreadPoolExecutorTest.java:162)
at concurrent.ch8_tools.ThreadPoolExecutorTest$1.call(ThreadPoolExecutorTest.java:158)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
四、 Tomcat 線程池
Tomcat 在哪裏用到了線程池呢
- LimitLatch 用來限流,可以控制大連接個數,類似 J.U.C 中的 Semaphore 後面再講
- Acceptor 只負責【接收新的 socket 連接】
- Poller 只負責監聽 socket channel 是否有【可讀的 I/O 事件】
- 一旦可讀,封裝一個任務對象(socketProcessor),提交給 Executor 線程池處理
- Executor 線程池中的工作線程終負責【處理請求】
Tomcat 線程池擴展了 ThreadPoolExecutor,行爲稍有不同
- 如果總線程數達到 maximumPoolSize
- 這時不會立刻拋 RejectedExecutionException 異常
- 而是再次嘗試將任務放入隊列,如果還失敗,才拋出 RejectedExecutionException 異常
public void execute(Runnable command, long timeout, TimeUnit unit) {
submittedCount.incrementAndGet();
try {
//嘗試執行任務,如果超出maximumPoolSize 會拋出RejectedExecutionException
super.execute(command);
} catch (RejectedExecutionException rx) {
if (super.getQueue() instanceof TaskQueue) {
final TaskQueue queue = (TaskQueue) super.getQueue();
try {
//Tomcat對異常進行了捕獲,再次嘗試將任務放入隊列
if (!queue.force(command, timeout, unit)) {
//如果還失敗,才拋出 RejectedExecutionException 異常
submittedCount.decrementAndGet();
throw new RejectedExecutionException("Queue capacity is full.");
}
} catch (InterruptedException x) {
submittedCount.decrementAndGet();
Thread.interrupted();
throw new RejectedExecutionException(x);
}
} else {
submittedCount.decrementAndGet();
throw rx;
}
}
}
Connector 配置
配置項 | 默認值 | 說明 |
---|---|---|
acceptorThreadCount | 1 | acceptor 線程數量 |
pollerThreadCount | 1 | poller 線程數量 |
minSpareThreads | 10 | 核心線程數,即 corePoolSize |
maxThreads | 200 | 大線程數,即 maximumPoolSize |
executor | — | Executor 名稱,用來引用下面的 Executor配置 |
Executor 線程配置
配置項 | 默認值 | 說明 |
---|---|---|
threadPriority | 5 | 線程優先級 |
daemon | true | 是否守護線程 |
minSpareThreads | 25 | 核心線程數,即 corePoolSize |
maxThreads | 200 | 最大線程數,即 maximumPoolSize |
maxIdleTime | 60000 | 線程生存時間,單位是毫秒,默認值即 1 分鐘 |
maxQueueSize | Integer.MAX_VALUE | 隊列長度,默認無界 |
prestartminSpareThreads | false | 核心線程是否在服務器啓動時啓動 |
Tomcat中的阻塞隊列默認是無界的,那麼如果按照Java線程池原有的規則,救濟線程是永遠用不到的。所以Tomcat更改了救濟線程和阻塞隊列的使用規則,規則如下:
五、Fork/Join
1、概念
Fork/Join 是 JDK 1.7 加入的新的線程池實現,它體現的是一種分治思想,適用於能夠進行任務拆分的 cpu 密集型 運算
所謂的任務拆分,是將一個大任務拆分爲算法上相同的小任務,直至不能拆分可以直接求解。跟遞歸相關的一些計 算,如歸併排序、斐波那契數列、都可以用分治思想進行求解
Fork/Join 在分治的基礎上加入了多線程,可以把每個任務的分解和合並交給不同的線程來完成,進一步提升了運 算效率
Fork/Join 默認會創建與 cpu 核心數大小相同的線程池
2、使用
提交給 Fork/Join 線程池的任務需要繼承 RecursiveTask(有返回值)或 RecursiveAction(沒有返回值),例如下 面定義了一個對 1~n 之間的整數求和的任務
class MyTask extends RecursiveTask<Integer> {
private Integer n;
public MyTask(Integer n) {
this.n = n;
}
@Override
protected Integer compute() {
//設置終止條件
if(n == 1){
TheadPrint.print("join() " + n);
return 1;
}
//拆分任務
MyTask t = new MyTask(n - 1);
//執行任務
t.fork();
TheadPrint.print("fork() " + n + " + " + t.n);
//獲取任務結果
Integer res = t.join() + n;
TheadPrint.print("join() " + n + " + " + t.n + " = " + res);
return res;
}
@Override
public String toString() {
return "MyTask{" +
"n=" + n +
'}';
}
}
測試:
public static void main(String[] args) {
ForkJoinPool pool = new ForkJoinPool(4);
System.out.println(pool.invoke(new MyTask(5)));
}
結果:
12:02:57:892 Thread[ForkJoinPool-1-worker-0,5,main]: fork() 2 + 1
12:02:57:894 Thread[ForkJoinPool-1-worker-0,5,main]: join() 1
12:02:57:895 Thread[ForkJoinPool-1-worker-0,5,main]: join() 2 + 1 = 3
12:02:57:895 Thread[ForkJoinPool-1-worker-3,5,main]: fork() 3 + 2
12:02:57:895 Thread[ForkJoinPool-1-worker-3,5,main]: join() 3 + 2 = 6
12:02:57:896 Thread[ForkJoinPool-1-worker-1,5,main]: fork() 5 + 4
12:02:57:917 Thread[ForkJoinPool-1-worker-2,5,main]: fork() 4 + 3
12:02:57:920 Thread[ForkJoinPool-1-worker-2,5,main]: join() 4 + 3 = 10
12:02:57:921 Thread[ForkJoinPool-1-worker-1,5,main]: join() 5 + 4 = 15
15
用圖來表示如下圖,雖然上圖的打印順序不是順序的,但是執行的時候仍是順序執行的,如t1需要等待t2執行完畢才能得到結果。
因此任務切分十分關鍵,下面就是求和的改進
class AddTask3 extends RecursiveTask<Integer> {
int begin;
int end;
public AddTask3(int begin, int end) {
this.begin = begin;
this.end = end;
}
@Override
public String toString() {
return "{" + begin + "," + end + '}';
}
@Override
protected Integer compute() {
if (begin == end) {
log.debug("join() {}", begin);
return begin;
}
if (end - begin == 1) {
log.debug("join() {} + {} = {}", begin, end, end + begin);
return end + begin;
}
int mid = (end + begin) / 2;
AddTask3 t1 = new AddTask3(begin, mid);
t1.fork();
AddTask3 t2 = new AddTask3(mid + 1, end);
t2.fork();
log.debug("fork() {} + {} = ?", t1, t2);
int result = t1.join() + t2.join();
log.debug("join() {} + {} = {}", t1, t2, result);
return result;
}
}
然後提交給 ForkJoinPool 來執行
public static void main(String[] args) {
ForkJoinPool pool = new ForkJoinPool(4);
System.out.println(pool.invoke(new AddTask3(1, 10)));
}
結果
[ForkJoinPool-1-worker-0] - join() 1 + 2 = 3
[ForkJoinPool-1-worker-3] - join() 4 + 5 = 9
[ForkJoinPool-1-worker-0] - join() 3
[ForkJoinPool-1-worker-1] - fork() {1,3} + {4,5} = ?
[ForkJoinPool-1-worker-2] - fork() {1,2} + {3,3} = ?
[ForkJoinPool-1-worker-2] - join() {1,2} + {3,3} = 6
[ForkJoinPool-1-worker-1] - join() {1,3} + {4,5} = 15
15
用圖來表示
teger> {
int begin;
int end;
public AddTask3(int begin, int end) {
this.begin = begin;
this.end = end;
}
@Override
public String toString() {
return "{" + begin + "," + end + '}';
}
@Override
protected Integer compute() {
if (begin == end) {
log.debug("join() {}", begin);
return begin;
}
if (end - begin == 1) {
log.debug("join() {} + {} = {}", begin, end, end + begin);
return end + begin;
}
int mid = (end + begin) / 2;
AddTask3 t1 = new AddTask3(begin, mid);
t1.fork();
AddTask3 t2 = new AddTask3(mid + 1, end);
t2.fork();
log.debug("fork() {} + {} = ?", t1, t2);
int result = t1.join() + t2.join();
log.debug("join() {} + {} = {}", t1, t2, result);
return result;
}
}
然後提交給 ForkJoinPool 來執行
```java
public static void main(String[] args) {
ForkJoinPool pool = new ForkJoinPool(4);
System.out.println(pool.invoke(new AddTask3(1, 10)));
}
結果
[ForkJoinPool-1-worker-0] - join() 1 + 2 = 3
[ForkJoinPool-1-worker-3] - join() 4 + 5 = 9
[ForkJoinPool-1-worker-0] - join() 3
[ForkJoinPool-1-worker-1] - fork() {1,3} + {4,5} = ?
[ForkJoinPool-1-worker-2] - fork() {1,2} + {3,3} = ?
[ForkJoinPool-1-worker-2] - join() {1,2} + {3,3} = 6
[ForkJoinPool-1-worker-1] - join() {1,3} + {4,5} = 15
15
用圖來表示