溫故知新-java多線程&深入理解線程池



摘要

本文主要回顧java的JDK中的多線程的常見用法和深入理解線程池;

java中的線程

  • 創建線程的3種方式
  1. 通過實現 Runnable 接口來創建線程
  2. 通過繼承Thread來創建線程
  3. 通過 Callable 和 Future 創建線程
  • 開啓線程的方法
    • public void start() 使該線程開始執行;Java 虛擬機調用該線程的 run 方法。
    • public void run() 如果該線程是使用獨立的 Runnable 運行對象構造的,則調用該 Runnable 對象的 run 方法;否則,該方法不執行任何操作並返回。
  • 線程的狀態
public enum State {
   NEW,
   RUNNABLE,
   BLOCKED,
   WAITING,
   TIMED_WAITING,
   TERMINATED;
}

java中的線程池

線程池技術

線程池(Thread Pool)是一種基於池化思想管理線程的工具,經常出現在多線程服務器中,如MySQL等,追根溯源-編程語言&性能優化 這篇文檔也指出優化性能的一種手段就是池化技術,今天就稍微展開一下,池話技術;

  • 池化的表現形式

池化技術在計算機領域中的表現爲:統一管理IT資源,包括服務器、存儲、和網絡資源等等。通過共享資源,使用戶在低投入中獲益。除去線程池,還有其他比較典型的幾種使用策略包括:
- 內存池(Memory Pooling):預先申請內存,提升申請內存速度,減少內存碎片。
- 連接池(Connection Pooling):預先申請數據庫、redis連接,提升申請連接的速度,降低系統的開銷。
- 實例池(Object Pooling):循環使用對象,減少資源在初始化和釋放時的昂貴損耗。

  • 線程池解決的問題

解決的核心問題就是資源管理問題。在併發環境下,系統不能夠確定在任意時刻中,有多少任務需要執行,有多少資源需要投入。


這種不確定性將帶來以下若干問題

  • 頻繁申請/銷燬資源和調度資源,將帶來額外的消耗,可能會非常巨大。
  • 對資源無限申請缺少抑制手段,易引發系統資源耗盡的風險。
  • 系統無法合理管理內部的資源分佈,會降低系統的穩定性。

反之就是解決的問題

  • 降低資源消耗:通過池化技術重複利用已創建的線程,降低線程創建和銷燬造成的損耗。
  • 提高響應速度:任務到達時,無需等待線程創建即可立即執行。
  • 提高線程的可管理性:線程是稀缺資源,如果無限制創建,不僅會消耗系統資源,還會因爲線程的不合理分佈導致資源調度失衡,降低系統的穩定性。使用線程池可以進行統一的分配、調優和監控。

線程池的實現原理

簡述

Java中的線程池核心實現類是ThreadPoolExecutor,JDK 1.8的源碼重的ThreadPoolExecutor的UML類圖,瞭解下ThreadPoolExecutor的繼承關係。
在這裏插入圖片描述

  • Executor

void execute(Runnable command);
只有一個execute方法,只需提供Runnable對象

  • ExecutorService

ExecutorService接口繼承了Executor接口,並聲明瞭一些方法:submit、invokeAll、invokeAny以及shutDown等;

  • AbstractExecutorService

抽象類AbstractExecutorService實現了ExecutorService接口,基本實現了ExecutorService中聲明的所有方法;

  • ThreadPoolExecutor

ThreadPoolExecutor繼承了類AbstractExecutorService,execute()、submit()、shutdown()、shutdownNow()

ThreadPoolExecutor是如何運行的?

ThreadPoolExecutor是如何運行,如何同時維護線程和執行任務的呢?
運行圖
線程池在內部實際上構建了一個生產者消費者模型,將線程和任務兩者解耦,並不直接關聯,從而良好的緩衝任務,複用線程。線程池的運行主要分成兩部分:任務管理、線程管理。任務管理部分充當生產者的角色,當任務提交後,線程池會判斷該任務後續的流轉,策略如下:
(1)直接申請線程執行該任務;
(2)緩衝到隊列中等待線程執行;
(3)拒絕該任務。
線程管理部分是消費者,它們被統一維護在線程池內,根據任務請求進行線程的分配,當線程執行完任務後則會繼續獲取新的任務去執行,最終當線程獲取不到任務的時候,線程就會被回收。

線程池運行的狀態和線程數量

  • 線程池運行的狀態和線程數量

狀態和線程數量是伴隨着線程池的運行,由內部來維護。線程池內部使用一個變量維護兩個值:運行狀態(runState)和線程數量 (workerCount)。在具體實現中,線程池將運行狀態(runState)、線程數量 (workerCount)兩個關鍵參數的維護放在了一起
英文描述:The main pool control state, ctl, is an atomic integer packing two conceptual fields workerCount, indicating the effective number of threads runState, indicating whether running, shutting down etc

--- 線程池運行的狀態和數量---
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// Packing and unpacking ctl
private static int runStateOf(int c)     { return c & ~CAPACITY; }
private static int workerCountOf(int c)  { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }

---狀態值---
// runState is stored in the high-order bits
private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;

狀態枚舉

  • 狀態切換
    狀態轉換

任務執行機制

任務調度是線程池的主要入口,當用戶提交了一個任務,接下來這個任務將如何執行都是由這個階段決定的。任務的調度都是由execute方法完成的,這部分完成的工作是:檢查現在線程池的運行狀態、運行線程數、運行策略,決定接下來執行的流程,是直接申請線程執行,或是緩衝到隊列中執行,亦或是直接拒絕該任務。其執行過程如下:

  • 首先檢測線程池運行狀態,如果不是RUNNING,則直接拒絕,線程池要保證在RUNNING的狀態下執行任務。
  • 如果workerCount < corePoolSize,則創建並啓動一個線程來執行新提交的任務。
  • 如果workerCount >= corePoolSize,且線程池內的阻塞隊列未滿,則將任務添加到該阻塞隊列中。
  • 如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且線程池內的阻塞隊列已滿,則創建並啓動一個線程來執行新提交的任務。
  • 如果workerCount >= maximumPoolSize,並且線程池內的阻塞隊列已滿, 則根據拒絕策略來處理該任務, 默認的處理方式是直接拋異常。
  • JDK源碼如下:
    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }

隊列緩存

任務緩衝模塊是線程池能夠管理任務的核心部分。通過源碼可以看到緩存使用的隊列是BlockingQueue;

private final BlockingQueue workQueue;

  • 各種隊列的特點
    隊列的特點
  • 拒絕策略
    拒絕策略

Worker線程管理

Worker線程

private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
	/** Thread this worker is running in.  Null if factory fails. */
	final Thread thread;
	/** Initial task to run.  Possibly null. */
	Runnable firstTask;
	/** Per-thread task counter */
	volatile long completedTasks;
	...
	...
}

Worker這個工作線程,實現了Runnable接口,並持有一個線程thread,一個初始化的任務firstTask。thread是在調用構造方法時通過ThreadFactory來創建的線程,可以用來執行任務;firstTask用它來保存傳入的第一個任務,這個任務可以有也可以爲null。如果這個值是非空的,那麼線程就會在啓動初期立即執行這個任務,也就對應核心線程創建時的情況;如果這個值是null,那麼就需要創建一個線程去執行任務列表(workQueue)中的任務,也就是非核心線程的創建。
流轉過程
​#### 線程的生命週期
線程池需要管理線程的生命週期,需要在線程長時間不運行的時候進行回收。線程池使用一張Hash表去持有線程的引用,這樣可以通過添加引用、移除引用這樣的操作來控制線程的生命週期。這個時候重要的就是如何判斷線程是否在運行。

/**
* Set containing all worker threads in pool. Accessed only when
* holding mainLock.
*/
private final HashSet<Worker> workers = new HashSet<Worker>();

​Worker是通過繼承AQS,使用AQS來實現獨佔鎖這個功能。關於AQS,改天專門寫一篇博客;

  • Worker線程增加

通過閱讀源碼可以看到線程池是通過addWorker添加一個任務,添加有三種策略。

  • addWorker方法有兩個參數:firstTask、core。
  • firstTask參數用於指定新增的線程執行的第一個任務,該參數可以爲空;
  • core參數爲true表示在新增線程時會判斷當前活動線程數是否少於corePoolSize,false表示新增線程前需要判斷當前活動線程數是否少於maximumPoolSize
private boolean addWorker(Runnable firstTask, boolean core) {
...
}

添加的策略

/*
 * Proceed in 3 steps:
 *
 * 1. If fewer than corePoolSize threads are running, try to
 * start a new thread with the given command as its first
 * task.  The call to addWorker atomically checks runState and
 * workerCount, and so prevents false alarms that would add
 * threads when it shouldn't, by returning false.
 *
 * 2. If a task can be successfully queued, then we still need
 * to double-check whether we should have added a thread
 * (because existing ones died since last checking) or that
 * the pool shut down since entry into this method. So we
 * recheck state and if necessary roll back the enqueuing if
 * stopped, or start a new thread if there are none.
 *
 * 3. If we cannot queue task, then we try to add a new
 * thread.  If it fails, we know we are shut down or saturated
 * and so reject the task.
 */

添加時的檢查

/**
 * Checks if a new worker can be added with respect to current
 * pool state and the given bound (either core or maximum). If so,
 * the worker count is adjusted accordingly, and, if possible, a
 * new worker is created and started, running firstTask as its
 * first task. This method returns false if the pool is stopped or
 * eligible to shut down. It also returns false if the thread
 * factory fails to create a thread when asked.  If the thread
 * creation fails, either due to the thread factory returning
 * null, or due to an exception (typically OutOfMemoryError in
 * Thread.start()), we roll back cleanly.
 */

流程

  • Worker線程執行任務
    在Worker類中的run方法調用了runWorker方法來執行任務,runWorker方法的執行過程如下:
  1. while循環不斷地通過getTask()方法獲取任務。
  2. getTask()方法從阻塞隊列中取任務。
  3. 如果線程池正在停止,那麼要保證當前線程是中斷狀態,否則要保證當前線程不是中斷狀態。
  4. 執行任務。
  5. 如果getTask結果爲null則跳出循環,執行processWorkerExit()方法,銷燬線程。
/** Delegates main run loop to outer runWorker  */
public void run() {
    runWorker(this);
}
final void runWorker(Worker w) {
	Thread wt = Thread.currentThread();
	Runnable task = w.firstTask;
	w.firstTask = null;
	w.unlock(); // allow interrupts
	boolean completedAbruptly = true;
	try {
		while (task != null || (task = getTask()) != null) {
			w.lock();
			// If pool is stopping, ensure thread is interrupted;
			// if not, ensure thread is not interrupted.  This
			// requires a recheck in second case to deal with
			// shutdownNow race while clearing interrupt
			if ((runStateAtLeast(ctl.get(), STOP) ||
					(Thread.interrupted() &&
						runStateAtLeast(ctl.get(), STOP))) &&
				!wt.isInterrupted())
				wt.interrupt();
			try {
				beforeExecute(wt, task);
				Throwable thrown = null;
				try {
					task.run();
				} catch (RuntimeException x) {
					thrown = x;
					throw x;
				} catch (Error x) {
					thrown = x;
					throw x;
				} catch (Throwable x) {
					thrown = x;
					throw new Error(x);
				} finally {
					afterExecute(task, thrown);
				}
			} finally {
				task = null;
				w.completedTasks++;
				w.unlock();
			}
		}
		completedAbruptly = false;
	} finally {
		processWorkerExit(w, completedAbruptly);
	}
}

線程執行

  • Worker線程回收
    線程池中線程的銷燬依賴JVM自動的回收,線程池做的工作是根據當前線程池的狀態維護一定數量的線程引用,防止這部分線程被JVM回收,當線程池決定哪些線程需要回收時,只需要將其引用消除即可。Worker被創建出來後,就會不斷地進行輪詢,然後獲取任務去執行,核心線程可以無限等待獲取任務,非核心線程要限時獲取任務。當Worker無法獲取到任務,也就是獲取的任務爲空時,循環會結束,Worker會主動消除自身在線程池內的引用,從源碼中可以看到,線程回收的工作是在processWorkerExit方法完成的。
final void runWorker(Worker w) {
	try {

	} finally {
		processWorkerExit(w, completedAbruptly);
	}
}

流程圖如下

建線程池

  • 創建線程池的四個方法
public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
    
public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(),
                                    threadFactory));
}

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
   return new ScheduledThreadPoolExecutor(corePoolSize);
}
  • newSingleThreadExecutor

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

  • newFixedThreadPool

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

  • newCachedThreadPool

創建一個可緩存的線程池。如果線程池的大小超過了處理任務所需要的線程,線程的最大數量是Integer.MAX_VALUE;

  • newScheduledThreadPool

創建一個定長線程池,支持定時及週期性任務執行。線程的最大數量是Integer.MAX_VALUE;


  • 上面的四個最後都會調用ThreadPoolExecutor
public ThreadPoolExecutor(int corePoolSize,
	int maximumPoolSize,
	long keepAliveTime,
	TimeUnit unit,
	BlockingQueue < Runnable > workQueue,
	ThreadFactory threadFactory,
	RejectedExecutionHandler handler) {
	if (corePoolSize < 0 ||
		maximumPoolSize <= 0 ||
		maximumPoolSize < corePoolSize ||
		keepAliveTime < 0)
		throw new IllegalArgumentException();
	if (workQueue == null || threadFactory == null || handler == null)
		throw new NullPointerException();
	this.corePoolSize = corePoolSize;
	this.maximumPoolSize = maximumPoolSize;
	this.workQueue = workQueue;
	this.keepAliveTime = unit.toNanos(keepAliveTime);
	this.threadFactory = threadFactory;
	this.handler = handler;
}
  • int corePoolSize: 核心線程池大小
  • int maximumPoolSize: 最大核心線程池大小
  • long keepAliveTime: 超時了沒有人調用就會釋放
  • TimeUnit unit: 超時單位
  • BlockingQueue < Runnable > workQueue: 阻塞隊列
  • ThreadFactory threadFactory: 線程工廠
  • RejectedExecutionHandler handler: 拒絕策略

就是BlockingQueue的四種拒絕策略

參考

深入理解Java線程池:ThreadPoolExecutor
Java 多線程編程
Java線程池實現原理及其在美團業務中的實踐
從ReentrantLock的實現看AQS的原理及應用


你的鼓勵是我創作的最大動力

打賞

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