==> 學習彙總(持續更新)
==> 從零搭建後端基礎設施系列(一)-- 背景介紹
摘要:上篇實現了簡單的無鎖線程池,本篇開始實現有鎖線程池。先來思考一下,爲什麼線程池需要鎖?在沒有鎖的線程池中,就算是單線程提交,也可能會涉及到併發的問題,如果是多線程提交任務,這時候出錯的概率基本是百分百了。
一、什麼是鎖?
在開始寫代碼之前,先簡單認識一下鎖的本質是什麼,先來看一張圖。
中間那個管道就相當於鎖,不管外面有多少個線程,這個管道只能一一通過。
二、ThreadPoolV5
線程池參數:
- workers:工作線程
- corePoolSize:核心線程數
- maxPoolSize:最大線程數
- keepTimeAlive:線程空閒存活的時間
- TimeUnit:時間單位
- workerQueues:任務隊列
- RUNNING:線程池是否運行
- lock:全局鎖
- currentPoolSize:當前線程池大小
代碼:
public class ThreadPoolV6 {
//核心線程數
private int corePoolSize;
//最大線程數
private int maxPoolSize;
//允許線程的空閒時間
private long keepTimeAlive;
//存放工作線程的哈希表
private HashSet<Worker> workers;
//線程池是否關閉
private boolean RUNNING = true;
//任務隊列
private BlockingDeque<Runnable> workerQueues;
//全局鎖
private ReentrantLock lock = new ReentrantLock();
//記錄線程池大小
private int currentPoolSize = 0;
public ThreadPoolV6(int corePoolSize, int maxPoolSize, long keepTimeAlive, TimeUnit timeUnit, BlockingDeque<Runnable> workerQueues){
this.corePoolSize = corePoolSize;
this.maxPoolSize = maxPoolSize;
this.keepTimeAlive = timeUnit.toNanos(keepTimeAlive);
this.workers = new HashSet<>(corePoolSize);
this.workerQueues = workerQueues;
}
static int c = 0;
//執行任務
public void submit(Runnable task){
if(RUNNING){
/*
1.當前線程數小於核心線程數時,創建新的工作線程處理
2.當前線程數等於核心線程數時,加入任務隊列
3.當任務隊列滿時,創建新的工作線程
4.當工作線程達到最大線程數時,拒絕提交新的任務
*/
try {
lock.lock();
if(currentPoolSize < corePoolSize){
System.out.println("核心線程數:" + (++c));
addWorker(task);
//如果隊列滿了,會返回false
} else if(workerQueues.offer(task)){
} else if(currentPoolSize < maxPoolSize){
addWorker(task);
} else {
throw new RuntimeException("線程池已滿,拒絕提交任務");
}
}finally {
lock.unlock();
}
}
}
//關閉線程池
public void shutdown(){
try {
lock.lock();
RUNNING = false;
System.out.println("關閉前線程池大小:" + currentPoolSize);
for (Worker worker : workers) {
worker.thread.interrupt();
}
} finally {
lock.unlock();
}
}
//創建新的工作線程
private void addWorker(Runnable task){
Worker w = new Worker(task);
workers.add(w);
w.thread.start();
++currentPoolSize;
}
//工作線程類
private class Worker implements Runnable {
Thread thread;
Runnable task;
public Worker(Runnable task){
this.task = task;
this.thread = new Thread(this);
}
@Override
public void run() {
Runnable t = this.task;
this.task = null;
while (t != null || (t = getTask()) != null){
t.run();
t = null;
}
try {
lock.lock();
workers.remove(this);
} finally {
lock.unlock();
}
System.out.println("當前線程:" + Thread.currentThread().getName() + " 退出");
}
private Runnable getTask(){
boolean timeout = false;
for (;;){
//如果線程池關閉 並且 工作隊列爲空,那麼可以回收該線程
if(!RUNNING && workerQueues.isEmpty()) return null;
try {
//如果超時未拿到任務 並且 當前線程數大於核心線程數的時候,就可以回收該線程
boolean timed;
try {
lock.lock();
timed = workers.size() > corePoolSize;
if(timed && timeout) {
--currentPoolSize;
return null;
}
} finally {
lock.unlock();
}
Runnable runnable = timed ? workerQueues.poll(keepTimeAlive, TimeUnit.NANOSECONDS) : workerQueues.take();
if(runnable != null){
return runnable;
}
timeout = true;
} catch (InterruptedException e) {
timeout = false;
}
}
}
}
}
測試代碼:
public static void main(String[] args) throws InterruptedException {
ThreadPoolV6 pool = new ThreadPoolV6(4, 8, 1, TimeUnit.SECONDS, new LinkedBlockingDeque<>(20));
try {
for (int i = 0; i < 28; i++) {
new Thread(() -> pool.submit(() -> {
System.out.println("當前線程:" + Thread.currentThread().getName() + " 開始");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("當前線程:" + Thread.currentThread().getName() + " 結束");
})).start();
}
Thread.sleep(3000);
for (int i = 0; i < 8; i++) {
new Thread(() -> pool.submit(() -> {
System.out.println("當前線程:" + Thread.currentThread().getName() + " 開始");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("當前線程:" + Thread.currentThread().getName() + " 結束");
})).start();
}
} finally {
Thread.sleep(5000);
//關閉線程池
pool.shutdown();
}
}
測試結果:
問題分析:
看測試結果,加鎖後是符合預期的,非核心線程被回收,核心線程常駐內存。接下來分析還有哪些問題
submit
裏的加鎖方式,有些粗魯,相當於把整個方法都給鎖了,效率會大打折扣。這樣會造成任務提交的時候其實是串行提交的,效率上並無任何提高,還因爲加鎖解鎖而損耗了性能。- 再來看
getTask
裏的加鎖方式,思想是非常好的,只鎖住了一小段執行非常快的關鍵代碼,那就是判斷當前線程池大小是否大於核心線程數,如果大於就可以回收,否則只能調用take()
方法阻塞直到有新任務來臨。 - 再來看
shutdown()
方法,和無鎖線程池的區別是,多了線程中斷,這能解決什麼問題呢?首先,在getTask()
方法裏,take()
會阻塞,如果你僅僅只是修改RUNNING
的狀態,是不能夠關閉線程的。所以需要增加一個線程中斷,讓take()
從阻塞中拋異常,然後我們捕獲處理即可。又或者是線程正在執行任務,這時候可以根據中斷狀態,自行決定是立刻中斷還是執行完任務再中斷。 - 最後可以看看
submit
的返回值是void
,滿足不了需要監控返回值的場景。
根據這四個問題進行改進得到V6版
三、ThreadPoolV6
線程池參數:
- workers:工作線程
- corePoolSize:核心線程數
- maxPoolSize:最大線程數
- keepTimeAlive:線程空閒存活的時間
- TimeUnit:時間單位
- workerQueues:任務隊列
- RUNNING:線程池是否運行
- lock:全局鎖
- currentPoolSize:當前線程池大小
代碼:
public class ThreadPoolV6 {
//核心線程數
private int corePoolSize;
//最大線程數
private int maxPoolSize;
//允許線程的空閒時間
private long keepTimeAlive;
//存放工作線程的哈希表
private HashSet<Worker> workers;
//線程池是否關閉
private boolean RUNNING = true;
//任務隊列
private BlockingDeque<Runnable> workerQueues;
//全局鎖
private ReentrantLock lock = new ReentrantLock();
//記錄線程池大小
private AtomicInteger currentPoolSize = new AtomicInteger(0);
public ThreadPoolV8(int corePoolSize, int maxPoolSize, long keepTimeAlive, TimeUnit timeUnit, BlockingDeque<Runnable> workerQueues){
this.corePoolSize = corePoolSize;
this.maxPoolSize = maxPoolSize;
this.keepTimeAlive = timeUnit.toNanos(keepTimeAlive);
this.workers = new HashSet<>(corePoolSize);
this.workerQueues = workerQueues;
}
static int c = 0;
public <T> Future<T> submit(Runnable task){
RunnableFuture<T> ftask = new FutureTask<T>(task, null);
execute(ftask);
return ftask;
}
public <T> Future<T> submit(Callable<T> task){
RunnableFuture<T> ftask = new FutureTask<T>(task);
execute(ftask);
return ftask;
}
//執行任務
private void execute(Runnable task){
if(RUNNING){
/*
1.當前線程數小於核心線程數時,創建新的工作線程處理
2.當前線程數等於核心線程數時,加入任務隊列
3.當任務隊列滿時,創建新的工作線程
4.當工作線程達到最大線程數時,拒絕提交新的任務
*/
if(currentPoolSize.get() < corePoolSize && addWorker(task, true)){
System.out.println("核心線程數:" + (++c));
//如果隊列滿了,會返回false
} else if(workerQueues.offer(task)){
} else if(currentPoolSize.get() < maxPoolSize && addWorker(task, false)){
System.out.println("非核心線程數:" + (++c));
} else {
throw new RuntimeException("線程池已滿,拒絕提交任務");
}
}
}
//關閉線程池
public void shutdown(){
try {
lock.lock();
RUNNING = false;
System.out.println("關閉前線程池大小:" + currentPoolSize);
for (Worker worker : workers) {
worker.thread.interrupt();
}
} finally {
lock.unlock();
}
}
//創建新的工作線程
private boolean addWorker(Runnable task, boolean core){
for (;;){
int c = currentPoolSize.get();
if((core && c < corePoolSize) || !core && c < maxPoolSize){
if(currentPoolSize.compareAndSet(c, c + 1)){
break;
}
} else {
return false;
}
}
try {
lock.lock();
Worker w = new Worker(task);
workers.add(w);
w.thread.start();
} finally {
lock.unlock();
}
return true;
}
//工作線程類
private class Worker implements Runnable {
Thread thread;
Runnable task;
public Worker(Runnable task){
this.task = task;
this.thread = new Thread(this);
}
@Override
public void run() {
Runnable t = this.task;
this.task = null;
while (t != null || (t = getTask()) != null){
t.run();
t = null;
}
workers.remove(this);
System.out.println("當前線程:" + Thread.currentThread().getName() + " 退出");
}
private Runnable getTask(){
boolean timeout = false;
for (;;){
//如果線程池關閉 並且 工作隊列爲空,那麼可以回收該線程
if(!RUNNING && workerQueues.isEmpty()) return null;
try {
//如果超時未拿到任務 並且 當前線程數大於核心線程數的時候,就可以回收該線程
boolean timed;
try {
lock.lock();
timed = currentPoolSize.get() > corePoolSize;
if(timed && timeout) {
currentPoolSize.decrementAndGet();
return null;
}
} finally {
lock.unlock();
}
Runnable runnable = timed ? workerQueues.poll(keepTimeAlive, TimeUnit.NANOSECONDS) : workerQueues.take();
if(runnable != null){
return runnable;
}
timeout = true;
} catch (InterruptedException e) {
timeout = false;
}
}
}
}
}
測試代碼1:
同上
測試結果1:
同上
測試代碼2:
public static void main(String[] args) throws ExecutionException, InterruptedException {
ThreadPoolV6 pool = new ThreadPoolV6(10, 50, 1, TimeUnit.SECONDS, new LinkedBlockingDeque<>(30));
//ThreadPoolExecutor pool = new ThreadPoolExecutor(10, 50, 1, TimeUnit.SECONDS, new LinkedBlockingDeque<>(30));
long b = System.currentTimeMillis();
List<Future<Integer>> futures = new ArrayList<>();
for (int i = 0; i <= 80; i++) {
final int start = 1000000 * i, end = 1000000 * (i + 1);
futures.add(pool.submit(() -> solvePrime(start, end)));
}
int res = 0;
for (Future<Integer> future : futures) {
res += future.get();
}
long e = System.currentTimeMillis();
System.out.println("結果:" + res);
System.out.println("耗時:" + (e - b) / 1000.0);
}
static int solvePrime(int start, int end){
int c = 0;
for (int i = start; i <= end; i++) {
c = isPrime(i) ? c + 1 : c;
}
return c;
}
static boolean isPrime(int num){
for (int i = 2; i < num; i++) {
if(num % 2 == 0){
return false;
}
}
return true;
}
測試結果2:
V6線程池
java自帶線程池
問題分析:
看測試2,自帶線程居然比V6版效率低?其實在某些場景下,自己實現的線程池效率確實比官方的好,但是穩定性,可用性方面肯定是不如官方的,畢竟官方的實現邏輯非常嚴謹和經過大量測試的。我們來看問題。
-
submit
增加了返回值,就是那個熟悉的Future
,如果是Runable的任務,get()
返回的是null,如果是Callable的任務,就可以返回指定類型的值。 -
我們重點可以看到
addWorker
方法,首先返回值從void
變成了boolean
,這是爲什麼呢?設想一下,如果有10個任務同時提交,那麼execute
中的第一行判斷是不是都通過了,當前線程池大小是0,都小於核心線程數,所以addWorker
都會進去。看下面這段代碼,先通過CAS將線程池大小+1後,再進行實際的工作線程創建。private boolean addWorker(Runnable task, boolean core){ for (;;){ int c = currentPoolSize.get(); //如果是核心線程,那麼當前線程池大小必須小於corePoolSize //如果是非核心線程,那麼當前線程池大小必須小於maxPoolSize if((core && c < corePoolSize) || !core && c < maxPoolSize){ //將當前線程池大小+1,之後跳出循環,進行工作線程的創建和添加 if(currentPoolSize.compareAndSet(c, c + 1)){ break; } } else { return false; } } …… return true; }
-
最後我們再來回顧一下
execute
方法中的分支判斷//在這裏會出現併發addWorker的操作,但是僅僅有corePoolSize個返回true if(currentPoolSize.get() < corePoolSize && addWorker(task, true)){ System.out.println("核心線程數:" + (++c)); //當前面的addWorker返回false的時候,就會將任務加入隊列中 } else if(workerQueues.offer(task)){ //這裏也可能出現併發addWorker的操作,但是僅僅有maxPoolSize - corePoolSize個返回true } else if(currentPoolSize.get() < maxPoolSize && addWorker(task, false)){ System.out.println("非核心線程數:" + (++c)); } else { //當隊列滿了,並且最大線程數已經達到了,就會執行這個策略 throw new RuntimeException("線程池已滿,拒絕提交任務"); }
四、總結
線程池實現到V6版,已經將其核心思想造出來了,接下來不過是對性能的優化和功能性的擴展了。
- 線程池最核心最核心的思想是,在併發提交任務的場景下,實現對線程的高效管理和複用。
- 要理解線程池,先從無鎖入手,理解透徹它是如何管理和複用線程去執行新任務的。
- 要深入理解線程池,需要理解鎖,併發等知識,知道爲什麼需要鎖,哪裏需要鎖,以及如何鎖。
- 模仿現有的輪子的時候,不要一開始就從最難的開始,應該從最簡單的開始,然後一步一步的去仿造它的核心功能。
最後,關於併發處理,還是官方的🐂🍺,這裏只是將學到的皮毛展示了一下,下一篇將分析java自帶的線程池。