萬字長文詳解Java執行緒池面試題

王有志發表於2023-10-08

王有志,一個分享硬核 Java 技術的互金摸魚俠
加入 Java 人的提桶跑路群:共同富裕的Java人

今天是《麵霸的自我修養》第 6 篇文章,我們一起來看看面試中會問到哪些關於執行緒池的問題吧。

資料來源

  • 大部分來自於各機構(Java 之父,Java 繼父,某靈,某泡,某客)以及各博主整理文件;
  • 小部分來自於我以及身邊朋友的實際經理,題目上會做出標識,並註明面試公司。

疊“BUFF”

  • 八股文通常出現在面試的第一二輪,是“敲門磚”,但僅僅掌握八股文並不能幫助你拿下 Offer;
  • 由於本人水平有限,文中難免出現錯誤,還請大家以批評指正為主,儘量不要噴~~

執行緒池是什麼?為什麼要使用執行緒池?

難易程度:??

重要程度:?????

面試公司:無

計算機中,執行緒的建立和銷燬開銷較大,頻繁的建立和銷燬執行緒會影響程式效能。利用基於池化思想的執行緒池來統一管理和分配執行緒,複用已建立的執行緒,避免頻繁建立和銷燬執行緒帶來的資源消耗,提高系統資源的利用率

執行緒池具有以下 3 點優勢:

  • 降低資源消耗,重複利用已經建立的執行緒,避免執行緒建立與銷燬帶來的資源消耗;
  • 提高響應速度,接收任務時,可以透過執行緒池直接獲取執行緒,避免了建立執行緒帶來的時間消耗;
  • 便於管理執行緒,統一管理和分配執行緒,避免無限制建立執行緒,另外可以引入執行緒監控機制。

Java 中如何建立執行緒池?

難易程度:??

重要程度:????

面試公司:無

Java 中可以透過 ThreadPoolExecutor 和 Executors 建立執行緒池。

使用 ThreadPoolExecutor 建立執行緒池

使用 ThreadPoolExecutor 可以建立自定義執行緒池,例如:

ExecutorService threadPoolExecutor = new ThreadPoolExecutor(
	10, 
	20, 
	10, 
	TimeUnit.MILLISECONDS, 
	new LinkedBlockingQueue<Runnable>(),
	new ThreadPoolExecutor.AbortPolicy()
);

瞭解以上程式碼的含義前,我們先來看 ThreadPoolExecutor 提供的的構造方法:

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
	this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler);
}

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
	this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, defaultHandler);
}

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
	this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), handler);
}

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;
}

ThreadPoolExecutor 提供了 4 個構造方法,但最後都會指向含有 7 個引數的構造方法上。我們一一說明這些引數的含義:

  • int corePoolSize,執行緒池的核心執行緒數量,核心執行緒線上程池的生命週期中不會被銷燬
  • int maximumPoolSize,執行緒池的最大執行緒數量,超出核心執行緒數量的非核心執行緒
  • long keepAliveTime,執行緒存活時間,非核心執行緒空閒時存活的最大時間;
  • TimeUnit unit,keepAliveTime 的時間單位;
  • BlockingQueue<Runnable> workQueue,執行緒池的任務佇列;
  • ThreadFactory threadFactory,執行緒工廠,用於建立執行緒,可以自定義執行緒;
  • RejectedExecutionHandler handler,拒絕策略,當任務數量超出執行緒池的容量(超過 maximumPoolSize 並且 workQueue 已滿)時的處理策略。

使用 Executors 建立執行緒池

除了使用 ThreadPoolExecutor 建立執行緒池外,還可以透過 Executors 建立 Java 內建的執行緒池,Java 中提供了 6 種內建執行緒池:

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);

ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();

ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

ExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);

ExecutorService singleThreadScheduledExecutor = Executors.newSingleThreadScheduledExecutor();

ExecutorService workStealingPool = Executors.newWorkStealingPool();

Tips:關於這 6 種執行緒池的詳細解釋,參考下一題。


Java 中提供了哪些執行緒池?

難易程度:??

重要程度:????

面試公司:無

Java 中提供了 6 種執行緒池,可以透過 Executors 獲取。

FixedThreadPool

透過 Executors 建立 FixedThreadPool 的程式碼如下:

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);

FixedThreadPool 是固定執行緒數量的執行緒池,透過Executors#newFixedThreadPool方法獲得,原始碼如下:

public static ExecutorService newFixedThreadPool(int nThreads) {
	return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}

本質上是透過 ThreadPoolExecutor 建立的執行緒池,核心執行緒數和最大執行緒數相同,工作佇列使用 LinkedBlockingQueue,該佇列最大容量是 Integer.MAX_VALUE

SingleThreadExecutor

透過 Executors 建立 SingleThreadExecutor 的程式碼如下:

ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();

SingleThreadExecutor 是隻有一個執行緒的執行緒池,透過Executors#newSingleThreadExecutor方法獲得,其原始碼如下:

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

依舊是透過 ThreadPoolExecutor 建立的執行緒池,最大執行緒數和核心執行緒數設定為 1,工作佇列使用 LinkedBlockingQueue。SingleThreadExecutor 適合按順序執行的場景。

ScheduledThreadPool

透過 Executors 建立 ScheduledThreadPool 的程式碼如下:

ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(10);

ScheduledThreadPool 是具有定時排程和延遲排程能力的執行緒池,透過Executors#newScheduledThreadPool方法獲得,其原始碼如下:

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

與前兩個不同的是 ScheduledThreadPool 是用過 ScheduledThreadPoolExecutor 建立的,原始碼如下:

public class Executors {
	public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
		return new ScheduledThreadPoolExecutor(corePoolSize);
	}
}

public class ScheduledThreadPoolExecutor extends ThreadPoolExecutor implements ScheduledExecutorService {
	public ScheduledThreadPoolExecutor(int corePoolSize) {
		super(corePoolSize, Integer.MAX_VALUE, DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS, new DelayedWorkQueue());
	}
}

追根溯源的話 ScheduledThreadPoolExecutor 依舊是透過 ThreadPoolExecutor 的構造方法建立執行緒池的,能夠實現定時排程的特性是因為ScheduledThreadPoolExecutor#execute方法和ScheduledThreadPoolExecutor#schedule方法實現的:

public class ScheduledThreadPoolExecutor extends ThreadPoolExecutor implements ScheduledExecutorService {
	public void execute(Runnable command) {
		schedule(command, 0, NANOSECONDS);
	}

	public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
		if (command == null || unit == null)
			throw new NullPointerException();
		RunnableScheduledFuture<Void> t = decorateTask(command, new ScheduledFutureTask<Void>(command, null, triggerTime(delay, unit), sequencer.getAndIncrement()));
		delayedExecute(t);
		return t;
	}
}

因為 ScheduledThreadPoolExecutor 並不是執行緒池中的重點內容,這裡我們不過多討論原始碼的實現,我們接下來看 ScheduledThreadPoolExecutor 該如何使用:

SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
System.out.println("當前時間:" + simpleDateFormat.format(new Date()));

scheduledThreadPool.schedule(new Runnable() {
	@Override
	public void run() {
		System.out.println("執行時間:" + simpleDateFormat.format(new Date()) + ",延遲3秒執行");
	}
}, 3, TimeUnit.SECONDS);

scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
	@Override
	public void run() {
		System.out.println("執行時間:" + simpleDateFormat.format(new Date()) + ",每3秒執行一次");
	}
}, 0, 3, TimeUnit.SECONDS);

SingleThreadScheduledExecutor

透過 Executors 建立 ScheduledExecutorService 的程式碼如下:

ExecutorService singleThreadScheduledExecutor = Executors.newSingleThreadScheduledExecutor();

與 ScheduledThreadPool 相同 SingleThreadScheduledExecutor 也是具有定時排程和延遲排程能力的執行緒池,同樣的 SingleThreadScheduledExecutor 也是透過 ScheduledThreadPoolExecutor 建立的,不同之處在於 ScheduledThreadPool 並不限制核心執行緒的數量,而 SingleThreadScheduledExecutor 只會建立一個核心執行緒

CachedThreadPool

透過 Executors 建立 CachedThreadPool 的程式碼如下:

ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

CachedThreadPool 是可快取執行緒的執行緒池,透過Executors#newSingleThreadExecutor方法獲得,其原始碼如下:

public static ExecutorService newCachedThreadPool() {
	return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
}

CachedThreadPool 的特點是沒有核心執行緒,任務提交後會建立新執行緒執行,並且沒有最大數量限制,每個執行緒空閒後的存活時間是 60 秒,如果 60 秒內有新的任務提交,會複用這些執行緒,也就實現了執行緒快取的能力,工作佇列使用 SynchronousQueue,它不儲存任何元素,使用 SynchronousQueue 的目的是為了能夠立即建立(使用)非核心執行緒(涉及到 ThreadPoolExecutor 的實現原理,後文詳解)執行任務

WorkStealingPool

透過 Executors 建立 ScheduledExecutorService 的程式碼如下:

ExecutorService workStealingPool = Executors.newWorkStealingPool();

WorkStealingPool 是 Java 8 中加入的執行緒池,與之前的 5 種執行緒池直接或間接的迴歸到 ThreadPoolExecutor 不同,WorkStealingPool 是透過 ForkJoinPool 實現的(ForkJoinPool 與 ThreadPoolExecutor 都是 AbstractExecutorService 的實現),內部透過 Work-Stealing 演算法並行的處理任務,但無法保證任務的執行順序


Executor 框架是什麼?

難易程度:??

重要程度:????

面試公司:無

Executor 用於執行 Runnable(Callable)任務,提供了將 Runnable(Callable) 與執行機制解耦的能力,遮蔽了執行緒建立,排程方式等。Java 中的註釋是這樣描述 Executor 的:

An object that executes submitted Runnable tasks. This interface provides a way of decoupling task submission from the mechanics of how each task will be run, including details of thread use, scheduling, etc. An Executor is normally used instead of explicitly creating threads.

Executor 的體系:

Executor 體系中 ,ExecutorService 介面對 Executor 進行了擴充套件,提供了管理 Executor 和檢視 Executor 狀態的能力。

An Executor that provides methods to manage termination and methods that can produce a Future for tracking progress of one or more asynchronous tasks.

另外,我把 Executors 也加入到了 Executor 的體系結構中,雖然沒有繼承或者實現的關係,但是根據 Java 中的命名規範,Executors 是作為 Executor 的工具類出現的,從類圖中可以看到 Executors 提供了


執行緒池都有哪些狀態?

難易程度:??

重要程度:?????

面試公司:無

Java 中定義了執行緒池的 5 種狀態:

private static final int RUNNING    = -1 << COUNT_BITS; // 111 0 0000 0000 0000 0000 0000 0000 0000
private static final int SHUTDOWN   =  0 << COUNT_BITS; // 000 0 0000 0000 0000 0000 0000 0000 0000
private static final int STOP       =  1 << COUNT_BITS; // 001 0 0000 0000 0000 0000 0000 0000 0000
private static final int TIDYING    =  2 << COUNT_BITS; // 010 0 0000 0000 0000 0000 0000 0000 0000
private static final int TERMINATED =  3 << COUNT_BITS; // 011 0 0000 0000 0000 0000 0000 0000 0000

註釋中也非常詳細的解釋了每個狀態:

RUNNING: Accept new tasks and process queued tasks

SHUTDOWN: Don't accept new tasks, but process queued tasks

STOP: Don't accept new tasks, don't process queued tasks, and interrupt in-progress tasks

TIDYING: All tasks have terminated, workerCount is zero, the thread transitioning to state TIDYING will run the terminated() hook method

TERMINATED: terminated() has completed

  • RUNNING:接收新任務,並處理佇列中的任務;
  • SHUTDOWN:不接收新任務,僅處理佇列中的任務;
  • STOP:不接收新任務,不處理佇列中的任務,中斷正在執行的任務;
  • TIDYING:所有任務已經執行完畢,並且工作執行緒為 0,轉換到 TIDYING 狀態後將執行 Hook 方法ThreadPoolExecutor#terminated
  • TERMINATEDThreadPoolExecutor#terminated方法執行完畢,該狀態表示執行緒池徹底終止。

另外註釋中還詳細的描述了執行緒池狀態間轉換的規則:

RUNNING -> SHUTDOWN On invocation of shutdown()

(RUNNING or SHUTDOWN) -> STOP On invocation of shutdownNow()

SHUTDOWN -> TIDYING When both queue and pool are empty

STOP -> TIDYING When pool is empty

TIDYING -> TERMINATED When the terminated() hook method has completed

  • RUNNING -> SHUTDOWN:透過呼叫ThreadPoolExecutor#shutdown方法;
  • RUNNING 或 SHUTDOWN -> STOP:透過透過呼叫ThreadPoolExecutor#shutdownNow方法;
  • SHUTDOWN -> TIDYING:工作執行緒為 0(沒有處理中的任務),並且工作佇列中待處理的任務為 0 時;
  • STOP -> TIDYING:工作執行緒為 0 時(沒有處理中的任務);
  • TIDYING -> TERMINATED:TIDYING 狀態下,呼叫ThreadPoolExecutor#terminated方法。

最後我們透過一張圖來看下執行緒池狀態之間的轉換:


?執行緒池是如何實現的?

難易程度:?????

重要程度:?????

面試公司:阿里巴巴,美團,螞蟻金服

我們透過一段演示程式碼嘗試著推測執行緒池的執行過程,首先我們實現一個自定義的阻塞佇列:

public class CustomBlockingQueue<E> extends LinkedBlockingQueue<E> {

	public CustomBlockingQueue(int capacity) {
		super(capacity);
	}

	@Override
	public boolean offer(E e) {
		boolean result = super.offer(e);
		if (result) {
			System.out.println("新增到佇列中");
		}
		return result;
	}
}

CustomBlockingQueue 繼承自 LinkedBlockingQueue,並重寫了LinkedBlockingQueue#offer方法,在元素成功新增到佇列時輸出一行日誌,為的是能夠清晰的展示執行緒池中任務新增到工作佇列中的時機。

接著我們建立執行緒池:

ExecutorService threadPoolExecutor = new ThreadPoolExecutor(
	3,
	3,
	10,
	TimeUnit.SECONDS,
	new CustomBlockingQueue<>(3),
	new ThreadPoolExecutor.AbortPolicy());

執行緒池的容量是 9,即最大執行緒 6 個(核心執行緒 3 個,非核心執行緒 3 個,工作佇列容量為 3),拒絕策略是 AbortPolicy,超出執行緒池的容量後,直接丟棄任務。

使用這個執行緒池同時執行 10 個任務:

for(int i = 1; i <= 10; i++) {
	int finalI = i;
	threadPoolExecutor.execute(() -> {
		String threadName = Thread.currentThread().getName();
		System.out.println(threadName + ",開始執行任務:" + finalI);
		try {
			// 為了能夠長時間佔用執行緒
			TimeUnit.SECONDS.sleep(15);
		} catch (InterruptedException e) {
			throw new RuntimeException(e);
		}
		System.out.println(threadName + ",結束執行任務:" + finalI);
	});

	// 為了能夠實現按順序提交任務
	TimeUnit.SECONDS.sleep(1);
}

執行上述程式碼,可以看到列印的日誌為:

根據執行結果,並且結合之前對於執行緒池引數的解釋,我們就能推測出執行緒池的大致流程:

  1. 編號 1~3 的任務提交到執行緒池後,建立核心執行緒執行任務;
  2. 編號 4~6 的任務提交到執行緒池後,因為超出核心執行緒的數量,將任務新增到工作佇列中;
  3. 編號 7~9 的任務提交到執行緒池後,因為工作佇列已滿,建立非核心執行緒執行任務;
  4. 編號 10 的任務提交到執行緒池後,因為超出執行緒池的容量,執行拒絕策略;
  5. 編號 1~3 的任務執行完畢後,核心執行緒空閒,取出工作佇列中編號 4~6 的任務開始執行。

我們用一張圖來展示上述程式碼中任務執行的順序:

以上是我們透過案例來推測出的執行緒池工作流程,接下來我們透過原始碼分析來印證我們推測的結果,並梳理執行緒池執行過程中的具體細節。

執行緒池的原始碼分析

CTL 與執行緒池狀態

分析執行緒池執行流程前,我們先來看 ThreadPoolExecutor 中定義的主控狀態 CTL 和執行緒池狀態:

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); // 111 0 0000 0000 0000 0000 0000 0000 0000

private static final int COUNT_BITS = Integer.SIZE - 3;                 // 29
private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;            // 0001 1111 1111 1111 1111 1111 1111 1111

// 執行緒池狀態
private static final int RUNNING    = -1 << COUNT_BITS;                 // 111 0 0000 0000 0000 0000 0000 0000 0000
private static final int SHUTDOWN   =  0 << COUNT_BITS;                 // 000 0 0000 0000 0000 0000 0000 0000 0000
private static final int STOP       =  1 << COUNT_BITS;                 // 001 0 0000 0000 0000 0000 0000 0000 0000
private static final int TIDYING    =  2 << COUNT_BITS;                 // 010 0 0000 0000 0000 0000 0000 0000 0000
private static final int TERMINATED =  3 << COUNT_BITS;                 // 011 0 0000 0000 0000 0000 0000 0000 0000

// 計算CTL
private static int ctlOf(int rs, int wc) {
	return rs | wc;
}

// 獲取執行緒池狀態
private static int runStateOf(int c) { 
	return c & ~COUNT_MASK;
}

// 獲取執行緒池工作執行緒數
private static int workerCountOf(int c) {
	return c & COUNT_MASK;
}

Doug Lea 採用了位掩碼技術來設計 CTL,將 CTL 拆分成了 2 個部分,高 3 位表示執行緒池狀態,低 29 位表示工作執行緒數量,因此執行緒池最多允許 536870911 個執行緒處於工作狀態。

Java 中採用補碼的形式表示數字,最高位是符號位,0 表示正數,1 表示負數,RUNNING 狀態在二進位制表示中最高位是 1,是負數,其餘狀態的數值均為正數。結合上一題的分析,我們可以得到,執行緒池從執行狀態到終止狀態,隨著狀態的轉換,每種狀態的數值表示是逐漸增大的。

Tips:如果不熟悉位運算,可以參考我的另一篇文章《程式設計技巧:“高階”的位運算》。

執行緒池的任務執行

我們從任務執行的入口ThreadPoolExecutor#execute的原始碼開始:

public void execute(Runnable command) {
	// 校驗提交的任務
	if (command == null) {
		throw new NullPointerException();
	}

	// 獲取CTL
	int c = ctl.get();

	// STEP 1:工作執行緒數小於核心執行緒數
	if (workerCountOf(c) < corePoolSize) {
		if (addWorker(command, true)) {
			return;
		}
		// addWorker方法執行失敗時,重新獲取CTL
		c = ctl.get();
	}

	// STEP 2:工作執行緒數大於核心執行緒數,但可以新增到工作佇列中
	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);
		}
	} 
	// STEP 3:工作佇列中無法繼續新增任務
	else if (!addWorker(command, false)) {
		reject(command);
	}
}

ThreadPoolExecutor#execute原始碼中,可以將整體的執行邏輯分為 3 步(見註釋)。詳細分析這 3 步前,我們先來看頻繁出現的ThreadPoolExecutor#addWorker方法,由於該方法的原始碼較長,我們拆開來分析。

ThreadPoolExecutor#addWorker方法的宣告:

private boolean addWorker(Runnable firstTask, boolean core)

ThreadPoolExecutor#addWorker方法提供了兩個引數:

  • Runnable firstTask,要執行的任務
  • boolean core,表示是否為核心執行緒

接著是ThreadPoolExecutor#addWorker方法原始碼的第一部分,檢查執行緒池的狀態及執行緒的數量:

retry:
for (int c = ctl.get();;) {
	// 檢查執行緒池狀態
	if (runStateAtLeast(c, SHUTDOWN) && (runStateAtLeast(c, STOP) || firstTask != null || workQueue.isEmpty()))
		return false;
	for (;;) {
		// 檢查執行緒池的工作執行緒數量
		if (workerCountOf(c) >= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK))
			return false;
		// 增加執行緒池工作執行緒數量
		if (compareAndIncrementWorkerCount(c))
			break retry;
		c = ctl.get();
		if (runStateAtLeast(c, SHUTDOWN))
			continue retry;
	}
}

以上這部分程式碼是對執行緒池狀態的檢查,以及處理主控狀態 CTL,我們來分析關建程式碼:

  • 第 4 行,檢查執行緒池“最多”處於 SHUTDOWN 狀態,因為只有在 SHUTDOWN 狀態和 RUNNING 狀態中,才會建立執行緒執行任務;
  • 第 8 行,根據入參boolean core決定建立執行緒數量的上限是小於 corePoolSize 還是小於 maxmunPoolSize;
  • 第 11 行,呼叫ThreadPoolExecutor#compareAndIncrementWorkerCount方法來增加 CTL 中儲存的工作執行緒數量,成功後則跳出 retry 標籤;
  • 第 13 行,如果程式執行到這裡,修改 CTL 中工作執行緒數量失敗,需要重新獲取 CTL 並檢查執行緒池狀態。

注意ThreadPoolExecutor#compareAndIncrementWorkerCount方法採用了 CAS 技術,方法返回失敗說明此時 CTL 已經被其它執行緒修改,需要重新獲取 CTL 並檢查執行緒池狀態。

最後來看ThreadPoolExecutor#addWorker方法的第二部分原始碼,執行工作任務:

boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
	// 建立Worker物件
	w = new Worker(firstTask);
	// 獲取到Worker中建立的執行緒
	final Thread t = w.thread;
	if (t != null) {
		// 使用ReentrantLock加鎖
		final ReentrantLock mainLock = this.mainLock;
		mainLock.lock();
		try {
			int c = ctl.get();
			// 檢查執行緒池狀態,只有RUNNING狀態和STOP之下的狀態允許建立執行緒
			if (isRunning(c) || (runStateLessThan(c, STOP) && firstTask == null)) {
				// 檢查執行緒狀態
				if (t.getState() != Thread.State.NEW)
					throw new IllegalThreadStateException();
				workers.add(w);
				workerAdded = true;
				// 記錄執行緒池中出現的最大執行緒數量
				int s = workers.size();
				if (s > largestPoolSize)
					largestPoolSize = s;
			}
		} finally {
			mainLock.unlock();
		}
		if (workerAdded) {
			// 啟動執行緒,開始執行任務
			t.start();
			workerStarted = true;
		}
	}
} finally {
	if (! workerStarted)
		addWorkerFailed(w);
}
return workerStarted;

以上這部分程式碼是建立執行緒並執行任務的方法,我們來分析關鍵部分:

  • 第 6 行,建立 Worker 物件,我們可以直接將 Worker 物件與 Thread 物件畫上等號,認為建立了 Worker 物件就是建立了執行緒。
  • 第 16 行,依舊是對執行緒池狀態的檢查,這裡驗證了執行緒池狀態“最多”處於 SHUTDOWN 狀態,這個限制也不難理解,RUNNGING 狀態是執行緒池的正常狀態,允許建立執行緒並處理任務,SHUTDOWN 狀態下允許處理工作佇列中的執行緒,如果執行緒池中沒有活躍的執行緒,需要建立執行緒來處理。

需要注意,這部分程式碼中出現的ThreadPoolExecutor#runStateLessThan方法與之前出現的ThreadPoolExecutor#runStateAtLeast方法在比較 CTL 與狀態時在開閉區間上存在差異。

到這裡整個ThreadPoolExecutor#addWorker的方法也就差不多分析完了,它實際上就做了兩件事:

  • 檢查執行緒池,包括狀態檢查,執行緒池中執行緒數量的檢查
  • 建立 Worker 物件,並啟動(相當於建立 Thread 物件並啟動)

回過頭來,我們結合對ThreadPoolExecutor#addWorker方法的分析,不難看出ThreadPoolExecutor#execute方法的 3 步都做了什麼:

步驟 執行緒池狀態 工作執行緒數 佇列狀態 處理方式
STEP 1 RUNNING < corePoolSize 核心執行緒數未滿,建立核心執行緒處理任務
STEP2 RUNNING = corePoolSize 未飽和 核心執行緒數已滿,將任務新增到佇列中
STEP3 RUNNING >= corePoolSize 已飽和 核心執行緒和工作佇列已滿,建立非核心執行緒處理任務,建立失敗則執行拒絕策略

至此,我們已經能夠清晰的看到執行緒池的執行流程了,並且也可以印證我們透過演示程式碼來推測的執行緒池執行流程的正確性了。


執行緒池中的執行緒是什麼時間建立的?

難易程度:????

重要程度:???

面試公司:無

在上一題的分析中,我們已經知道執行緒池的執行緒是在提交任務後透過ThreadPoolExecutor#addWorker方法中建立的,具體是在 Worker 的構造方法中:

Worker(Runnable firstTask) {
	setState(-1);
	this.firstTask = firstTask;
	this.thread = getThreadFactory().newThread(this);
}

每個 Worker 物件中都持有一個透過 ThreadFactory 建立的執行緒,這也是為什麼我將執行緒池中的執行緒與 Worker 物件畫上等號。

除此之外,還可以呼叫ThreadPoolExecutor#prestartCoreThread來預建立執行緒,該方法原始碼如下:

public boolean prestartCoreThread() {
	return workerCountOf(ctl.get()) < corePoolSize && addWorker(null, true);
}

從原始碼中可以看到,每次呼叫只會預建立 1 個 Worker 物件(即 1 個核心執行緒),如果需要建立全部的核心執行緒,需要多次呼叫。


?執行緒池中的核心執行緒是如何複用的?

難易程度:?????

重要程度:?????

面試公司:阿里巴巴,美團,螞蟻金服

執行緒池是透過 Worker 物件來完成執行緒的複用的。我們來看內部類 Worker 的型別宣告:

private final class Worker extends AbstractQueuedSynchronizer implements Runnable;

Worker 自身繼承了 AQS,並實現了 Runnable 介面,說明 Worker 自身就是可被執行緒執行的。

接下來是 Worker 的構造方法:

Worker(Runnable firstTask) {
	setState(-1);
	this.firstTask = firstTask;
	this.thread = getThreadFactory().newThread(this);
}

可以看到,構造在建立執行緒時使用的 Runnable 物件是 Worker 物件自身,同時 Worker 物件透過成員變數 firstTask 儲存了我們提交的 Runnable 物件。

回到ThreadPoolExecutor#addWorker方法中,我們來看執行緒啟動的部分:

w = new Worker(firstTask);
final Thread t = w.thread;
t.start();

我們已經知道,Java 中呼叫Thread#start方法中,會執行Runnable#run方法,那麼在這段程式碼中執行的是誰的 run 方法呢?答案是 Worker 物件重寫的 run 方法。

我們來看Worker#run的原始碼:

private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
	public void run() {
		runWorker(this);
	}
}

Worker#run方法呼叫了 runWorker 方法,該方法並不是內部類 Worker 的,而是 ThreadPoolExecutor 的方法,接著來看原始碼:

final void runWorker(Worker w) {
	Thread wt = Thread.currentThread();
	// 獲取Worker物件中封裝的Runnable
	Runnable task = w.firstTask;
	w.firstTask = null;
	w.unlock();
	boolean completedAbruptly = true;
	try {
		while (task != null || (task = getTask()) != null) {
			w.lock();
			// 執行緒池狀態檢查
			if ((runStateAtLeast(ctl.get(), STOP) || (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) && !wt.isInterrupted())
				wt.interrupt();
			try {
				beforeExecute(wt, task);
				try {
					// 執行任務
					task.run();
					afterExecute(task, null);
				} catch (Throwable ex) {
					afterExecute(task, ex);
					throw ex;
				}
			} finally {
				task = null;
				w.completedTasks++;
				w.unlock();
			}
		}
		completedAbruptly = false;
	} finally {
		processWorkerExit(w, completedAbruptly);
	}
}

重點關注第 9 行的 while 迴圈,它有兩個條件:

  • task != null,即 Worker 物件自身儲存的 Runnable 不為 null;
  • (task = getTask()) != null,即透過ThreadPoolExecutor#getTask方法獲取到的 Runnable 不為 null。

第一個條件不難想到,即首次提交時 Worker 會“攜帶” Runnable,那麼第二個條件是從哪裡獲取到的待執行任務呢?答案是工作佇列

我們來看ThreadPoolExecutor#getTask方法的原始碼:

private Runnable getTask() {
	boolean timedOut = false;
	for (;;) {
		// 狀態檢查
		int c = ctl.get();
		if (runStateAtLeast(c, SHUTDOWN) && (runStateAtLeast(c, STOP) || workQueue.isEmpty())) {
			decrementWorkerCount();
			return null;
		}
		// 獲取工作執行緒數量,並判斷是否為非核心執行緒
		int wc = workerCountOf(c);
		boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
		if ((wc > maximumPoolSize || (timed && timedOut)) && (wc > 1 || workQueue.isEmpty())) {
			if (compareAndDecrementWorkerCount(c))
				return null;
			continue;
		}
		try {
			// 透過工作佇列獲取待執行任務
			Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take();
			if (r != null)
				return r;
			timedOut = true;
		} catch (InterruptedException retry) {
			timedOut = false;
		}
	}
}

先來關注第 12 行中變數 timed 的賦值邏輯,有兩個條件:

  • boolean allowCoreThreadTimeOut,ThreadPoolExecutor 的成員變數,可以透過ThreadPoolExecutor#allowCoreThreadTimeOut方法賦值,表示是否允許核心執行緒超時銷燬,預設值是 false;
  • wc > corePoolSize,即工作執行緒數量是否超出核心執行緒的數量限制。

timed 變數關係到第 20 行程式碼中獲取工作佇列中待執行任務的方式:

  • 如果允許銷燬核心執行緒,或工作執行緒數量已經超出核心執行緒數量限制,則透過workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS)的方式獲取待執行任務,該方法獲取隊首元素,如果當前佇列為空,則在等待指定時間後返回 null
  • 如果不允許銷燬核心執行緒,或工作執行緒數量小於核心執行緒數量,則透過workQueue.take()的方式獲取待執行任務,該方法獲取隊首元素,如果佇列為空,則進入等待,直到能夠獲取元素為止

知道了以上內容後,我們再來看ThreadPoolExecutor#runWorker方法的 while 迴圈,在不允許銷燬核心執行緒的執行緒池中,核心執行緒第一次執行 Worker 自身“攜帶”的任務,執行完畢後再次進入 while 迴圈,嘗試獲取工作佇列中的待執行任務,如果此時工作佇列中沒有待執行任務,則工作佇列進入阻塞,此時 runWorker 方法也進入阻塞,直到工作佇列能夠成功返回待執行任務

簡單來說,執行緒池的核心執行緒複用,在於核心執行緒啟動後就進入“不眠不休”的工作中,除了執行首次提交的任務外,還會不斷嘗試從工作佇列中獲取待執行任務,如果無法獲取就阻塞到能夠獲取任務為止。


執行緒池的非核心執行緒是什麼時候銷燬的?

難易程度:?????

重要程度:?????

面試公司:無

非核心執行緒的銷燬依舊是在ThreadPoolExecutor#runWorker方法中進行的。

我們回到ThreadPoolExecutor#getTask方法中:

private Runnable getTask() {
	// 標記是否超時
	boolean timedOut = false;
	for (;;) {
		int c = ctl.get();
		if (runStateAtLeast(c, SHUTDOWN) && (runStateAtLeast(c, STOP) || workQueue.isEmpty())) {
			decrementWorkerCount();
			return null;
		}
		int wc = workerCountOf(c);
		boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
		if ((wc > maximumPoolSize || (timed && timedOut)) && (wc > 1 || workQueue.isEmpty())) {
			if (compareAndDecrementWorkerCount(c))
				return null;
			continue;
		}
		try {
			Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take();
			if (r != null)
				return r;
			timedOut = true;
		} catch (InterruptedException retry) {
			timedOut = false;
		}
	}
}

假設現在的情況是非核心執行緒透過ThreadPoolExecutor#getTask方法獲取工作佇列中的待執行任務,而此時工作佇列中已經沒有待執行的任務了。

第一次進入迴圈時,第 11 行程式碼會因為wc> corePoolSize而將 timed 賦值為 true,此時wc == maximumPoolSize且 timedOut 為 false,並不滿足第 12 行的判斷條件,會直接來到第 18 行透過workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS)的方式從工作佇列中獲取待執行任務,在等待一定的時間後,工作佇列返回了 null,此時進入第 21 行,將 timedOut 賦值為 true 後再次進入迴圈。

第二次進入迴圈時,變數 timed 依舊被賦值為 true,與第一次進入迴圈不同的是,此時的 timedOut 為 ture,已經滿足第 12 行的判斷條件,執行 if 語句的內容,呼叫ThreadPoolExecutor#compareAndDecrementWorkerCount方法減少 CTL 中工作執行緒的數量,並且返回 null。

重點在這個返回的 null 上,當ThreadPoolExecutor#getTask返回 null 後,ThreadPoolExecutor#runWorker會跳出迴圈,執行 finally 中的ThreadPoolExecutor#processWorkerExit方法:

private void processWorkerExit(Worker w, boolean completedAbruptly) {
	if (completedAbruptly) 
		decrementWorkerCount();
	final ReentrantLock mainLock = this.mainLock;
	mainLock.lock();
	try {
		completedTaskCount += w.completedTasks;
		// 從workers中刪除worker
		workers.remove(w);
	} finally {
		mainLock.unlock();
	}
	// 嘗試修改執行緒池狀態
	tryTerminate();

	// 處理STOP之下的狀態
	int c = ctl.get();
	if (runStateLessThan(c, STOP)) {
		if (!completedAbruptly) {
			int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
			if (min == 0 && ! workQueue.isEmpty())
				min = 1;
			if (workerCountOf(c) >= min)
				return; 
		}
		addWorker(null, false);
	}
}

該方法中,從 ThreadPoolExecutor 的成員變數 workers 中刪除 Worker 物件後,其內部的執行緒也將執行完畢,隨後會呼叫Thread#exit方法來銷燬執行緒,即表示執行緒池中該執行緒已經銷燬。


ThreadPoolExecutor#submit 方法和 ThreadPoolExecutor#execute 方法有什麼區別?

難易程度:??

重要程度:???

面試公司:無

ThreadPoolExecutor 的實現中,ThreadPoolExecutor#submitThreadPoolExecutor#execute有 4 處區別:

ThreadPoolExecutor#submit ThreadPoolExecutor#execute
方法定義 ExecutorService 介面中定義 Executor 介面中定義
返回值 有返回值 無返回值
任務型別 Runnable 任務和 Callable 任務 Runnable 任務
實現 AbstractExecutorService 中實現 ThreadPoolExecutor 中實現

Java 中提供了哪些拒絕策略?

難易程度:??

重要程度:???

面試公司:無

Java 中提供了 4 種拒絕策略:

  • CallerRunsPolicy:由呼叫ThreadPoolExecutor#execute方法的執行緒執行該任務;
  • AbortPolicy:直接丟擲異常;
  • DiscardPolicy:丟棄當前任務;
  • DiscardOldestPolicy:丟棄工作佇列中隊首的任務,即最早加入佇列中的任務,並將當前任務新增到佇列中。

這 4 種拒絕策略被定義為 ThreadPoolExecutor 的內部類,原始碼如下:

public class ThreadPoolExecutor extends AbstractExecutorService {
	public static class CallerRunsPolicy implements RejectedExecutionHandler {
		public CallerRunsPolicy() { }

		public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
			if (!e.isShutdown()) {
				r.run();
			}
		}
	}

	public static class AbortPolicy implements RejectedExecutionHandler {
		public AbortPolicy() { }

		public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
			throw new RejectedExecutionException("Task " + r.toString() + " rejected from " + e.toString());
		}
	}

	public static class DiscardPolicy implements RejectedExecutionHandler {
		public DiscardPolicy() { }

		public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
		}
	}

	public static class DiscardOldestPolicy implements RejectedExecutionHandler {
		public DiscardOldestPolicy() { }

		public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
			if (!e.isShutdown()) {
				e.getQueue().poll();
				e.execute(r);
			}
		}
	}
}

如果 Java 內建的拒絕策略無法滿足業務需求,可以自定義拒絕策略,只需要實現 RejectedExecutionHandler 介面即可。


?什麼是阻塞佇列?

難易程度:????

重要程度:???

面試公司:螞蟻金服

阻塞佇列是一種特殊的佇列,相較於普通的佇列,阻塞佇列提供了一個額外的功能,當佇列為空時,獲取佇列中元素的執行緒會被阻塞

Java 中提供了 7 種阻塞佇列,全部是繼承自 BlockingQueue 介面:

BlockingQueue 介面繼承了 Queue 介面,而 Queue 介面繼承了 Collection 介面。使用時,如果需要阻塞佇列的能力,要選擇單獨定義在 BlockingQueue 介面中的方法。

以下是阻塞佇列在實現不同介面方法時的差異:

操作 方法 特點
Collection 介面 新增元素 boolean add(E e) 失敗時丟擲異常
刪除元素 boolean remove(Object o) 失敗時丟擲異常
Queue 介面 入隊操作 boolean offer(E e) 成功返回 true,失敗返回 false
出隊操作 E poll() 成功返回出隊元素,失敗返回 null
BlockingQueue 介面 入隊操作 void put(E e) 無法入隊時阻塞當前執行緒,直到入隊成功
boolean offer(E e, long timeout, TimeUnit unit) 無法入隊時阻塞當前執行緒,直到入隊成功或超時
出隊操作 E take() 佇列中無元素時阻塞當前執行緒,直到有元素可出隊
E poll(long timeout, TimeUnit unit) 佇列中無元素時阻塞當前執行緒,直到有元素可出隊或超時

接下來我們來看這 7 種阻塞佇列的特點。

ArrayBlockingQueue

ArrayBlockingQueue 底層使用陣列作為儲存結構,需要指定阻塞佇列的容量(陣列的特點),且不具備擴容能力。除此之外,ArrayBlockingQueue 還提供了公平訪問和非公平訪問兩種模式,預設使用非公平訪問模式

// 非公平訪問的ArrayBlockingQueue
ArrayBlockingQueue<Runnable> arrayBlockingQueue = new ArrayBlockingQueue<>(1000);

// 公平訪問模式的ArrayBlockingQueue
ArrayBlockingQueue<Runnable> arrayBlockingQueue = new ArrayBlockingQueue<>(1000, true);

公平訪問模式指的是,在佇列中無元素時阻塞的執行緒,會在佇列可用時按照阻塞的先後順序訪問佇列;而非公平訪問模式下,則是隨機一個執行緒獲取訪問許可權。公平訪問模式保證了先後順序,但是為了維護這個先後順序,需要付出額外的效能作為代價。

需要額外注意的是,ArrayBlockingQueue 在入隊和出隊操作上使用同一把鎖,也就是說一個執行緒的入隊操作,不僅會阻塞其它執行緒的入隊操作,還會阻塞其它執行緒的出隊操作

LinkedBlockingQueue

LinkedBlockingQueue 底層使用單向連結串列作為儲存結構,並且提供了預設的容量Integer.MAX_VALUE,即在不指定容量時 LinkedBlockingQueue 是“無限大”的,也即是常說的無界佇列

LinkedBlockingQueue<Runnable> linkedBlockingQueue = new LinkedBlockingQueue<>();

LinkedBlockingQueue<Runnable> linkedBlockingQueue = new LinkedBlockingQueue<>(1000);

LinkedBlockingQueue 內部使用兩把獨立的鎖來控制入隊和出隊,這意味著入隊和出隊操作是相互獨立的,並不會互相阻塞

public class LinkedBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable {
	private final ReentrantLock takeLock = new ReentrantLock();

	private final ReentrantLock putLock = new ReentrantLock();
}

PriorityBlockingQueue

PriorityBlockingQueue 底層使用陣列作為儲存結構,預設容量為 11,支援自動擴容,支援元素按照優先順序排序,可以透過自定義實現 Comparator 介面來實現排序功能

例如,建立 Student 類,並允許根據 studenId 進行排序:

@Getter
@Setter
public class Student implements Comparable<Student> {

	private Integer studentId;

	private String name;

	@Override
	public int compareTo(Student student) {
		return this.studentId.compareTo(student.studentId);
	}

	public static void main(String[] args) {
		PriorityBlockingQueue<Student> priorityBlockingQueue = new PriorityBlockingQueue<>();

		for(int i = 0; i < 10; i++) {
			Random random = new Random();
			Student student = new Student();
			Integer studentId = random.nextInt(1000);
			student.setStudentId(studentId);
			student.setName("name-"+ studentId);

			priorityBlockingQueue.put(student);
		}
	}
}

注意,PriorityBlockingQueue 在入隊後並不能完全保證佇列中元素的優先順序順序,即佇列中儲存的元素可能是亂序的,但在出隊時是按照優先順序順序出隊

DelayQueue

DelayQueue 底層使用 PriorityQueue 作為儲存結構,因此 DelayQueue 具有 PriorityQueue 的特點,比如預設容量為 11,支援自動擴容,支援元素按照優先順序排序。DelayQueue 自身的特點是延時獲取元素,即在元素建立指定時間後才能夠從佇列中獲取元素,為了實現這個功能,入隊的元素必須實現 Delayed 介面

我們來建立一個實現 Delayed 介面的元素:

public class DelayedElement implements Delayed {

	private final Long createTime;

	private final Long delayTIme;

	private final TimeUnit delayTimeUnit;

	public DelayedElement(long delayTime, TimeUnit delayTimeUnit) {
		this.createTime = System.currentTimeMillis();
		this.delayTIme = delayTime;
		this.delayTimeUnit = delayTimeUnit;
	}

	@Override
	public long getDelay(TimeUnit unit) {
		long duration = createTime + TimeUnit.MILLISECONDS.convert(this.delayTIme, this.delayTimeUnit) - System.currentTimeMillis();
		return unit.convert(duration, TimeUnit.MILLISECONDS);
	}

	@Override
	public int compareTo(Delayed o) {
		DelayedElement delayedElement = (DelayedElement) o;
		return this.createTime.compareTo(delayedElement.createTime);
	}

注意Delayed#getDelay方法的實現,這裡的返回值並不是固定的延時時間,而是期望的延時時間與當前時間的差值,只有差值小於等於 0 時該元素才能出隊。這裡我選擇在構造方法中傳入延時時間與時間單位,並記錄物件的建立時間,Delayed#getDelay方法透過 (建立時間+延時時間-當前時間)的方式計算差值。至於Comparable#compareTo方法的實現,與 DelayQueue 的使用與其它阻塞佇列並無差別。

SynchronousQueue

SynchronousQueue 不儲存元素,它的每次入隊操作都要對應一次出隊操作,否則不能繼續新增元素

注意,SynchronousQueue 在入隊成功後會阻塞執行緒,直到其他執行緒執行出隊操作,同樣的,出隊操作時如果沒有提前執行入隊操作也會被阻塞,也就是說無法在一個執行緒中完成 SynchronousQueue 的入隊操作和出隊操作。

SynchronousQueue<Integer> synchronousQueue = new SynchronousQueue<>();

new Thread(() -> {
	for (int i = 0; i < 10; i++) {
		try {
			synchronousQueue.put(i);
			System.out.println("put-" + i);
		} catch (InterruptedException e) {
			throw new RuntimeException(e);
		}
	}
}).start();

TimeUnit.MILLISECONDS.sleep(100);

new Thread(() -> {
	for (int i = 0; i < 10; i++) {
		try {
			System.out.println("take-" + synchronousQueue.take());
		} catch (InterruptedException e) {
			throw new RuntimeException(e);
		}
	}
}).start();

LinkedTransferQueue

LinkedTransferQueue 的底層使用單向連結串列作為儲存結構,相較於其他阻塞佇列 LinkedTransferQueue 還實現了 TransferQueue 介面,實現了類似於 SynchronousQueue 傳遞元素的功能,但與 SynchronousQueue 不同的是,LinkedTransferQueue 是能夠儲存元素的。

LinkedTransferQueue 實現了 TransferQueue 介面的 3 個方法:

public interface TransferQueue<E> extends BlockingQueue<E> {
	
	void transfer(E e) throws InterruptedException;

	boolean tryTransfer(E e);
	
	boolean tryTransfer(E e, long timeout, TimeUnit unit) throws InterruptedExceptio)throws InterruptedException;
}

以上 3 個方法,在消費者等待獲取元素(呼叫 take 方法或 poll 方法)時,會立刻將元素傳遞給消費者,它們 3 者的差異在於沒有消費者等待獲取元素時的處理方式:

  • transfer 方法,該方法會阻塞執行緒,直到傳入的元素被消費;
  • tryTransfer 方法,該方法會直接返回 false,並且放棄該元素(即不會儲存到佇列中);
  • 帶有超時時間的 tryTransfer 方法,等待指定的時間,如果依舊沒有消費者消費該元素,返回 false。

我們寫個例子來測試LinkedTransferQueue#tryTransfer方法:

LinkedTransferQueue<Integer> linkedTransferQueue = new LinkedTransferQueue<>();

new Thread(() -> {
    for (int i = 0; i < 10; i++) {
        if (i == 3) {
            linkedTransferQueue.tryTransfer(i);
        } else {
            linkedTransferQueue.put(i);
        }
    }
}).start();

TimeUnit.SECONDS.sleep(2);

for (int i = 0; i < 10; i++) {
    System.out.println("take-" + linkedTransferQueue.take());
}

啟動執行緒向 linkedTransferQueue 中新增元素,第 4 個元素呼叫LinkedTransferQueue#tryTransfer來傳遞元素,主執行緒等待兩秒後從 linkedTransferQueue 中取出元素,可以看到佇列中並未儲存第 4 個元素。

另外,如果 LinkedTransferQueue 中已經存在元素,take 方法或者 poll 方法優先獲取的是隊首元素,而不是透過LinkedTransferQueue#transfeLinkedTransferQueue#tryTransfe傳入的元素。

LinkedBlockingDeque

LinkedBlockingDeque 底層使用雙向連結串列作為儲存結構,與 LinkedBlockingQueue 相同,LinkedBlockingDeque 的預設容量為Integer.MAX_VALUE,不同的是 LinkedBlockingDeque 實現了 BlockingDeque 介面,允許操作隊首和隊尾的元素

LinkedBlockingDeque 的方法中,名稱中含有 first 或 last 的公有方法均實現自 BlockingDeque 介面或 Deque 介面,例如:

public class LinkedBlockingDeque<E> extends AbstractQueue<E> implements BlockingDeque<E>, java.io.Serializable {
	public void addFirst(E e) {}
	
	public void addLast(E e) {}
	
	public boolean offerFirst(E e) {}
	
	public boolean offerLast(E e) {}

	public void putFirst(E e) throws InterruptedException{}

	public void putLast(E e) throws InterruptedException {}

	public E pollFirst() {}

	public E pollLast() {}

	public E takeFirst() throws InterruptedException {}

	public E takeLast() throws InterruptedException {}
}

什麼是無界佇列?使用無界佇列會出現什麼問題?

難易程度:???

重要程度:???

面試公司:無

無界佇列指的是佇列的容量是否為“無限大”,當然這並不是真正意義上無限大,而是指一個非常大的值。例如,建立 LinkedBlockingQueue 時,如果使用無參構造器,LinkedBlockingQueue 的容量會被設定為Integer.MAX_VALUE,那麼它就是無界佇列。

LinkedBlockingQueue 構造方法原始碼如下:

public class LinkedBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable {
	public LinkedBlockingQueue() {
		this(Integer.MAX_VALUE);
	}

	public LinkedBlockingQueue(int capacity) {
		if (capacity <= 0) {
			throw new IllegalArgumentException();
		}
		this.capacity = capacity;
		last = head = new Node<E>(null);
	}
}

Integer.MAX_VALUE的值約為 21 億,當程式異常時或併發較大的情況下,可能會無限制的向阻塞佇列中新增任務,導致記憶體溢位。通常在設定執行緒池的工作佇列時,需要根據具體的需求來計算出較為合適的佇列容量。

另外,在 Java 的內建執行緒池中,部分執行緒池使用了 LinkedBlockingQueue 且未指定容量,如:Executors#newFixedThreadPoolExecutors#newSingleThreadExecutor中建立的執行緒池使用了未指定容量的 LinkedBlockingQueue,這兩個執行緒池中的佇列即為無界佇列。這也是為什麼阿里巴巴在《Java 開發手冊》中提示到,不要使用 Java 內建執行緒池的一個原因。


?如何合理的設定執行緒池的引數?

難易程度:?????

重要程度:?????

面試公司:阿里巴巴,美團,螞蟻金服

Java 提供了 7 個引數來實現自定義執行緒池:

  • int corePoolSize:核心執行緒數量
  • int maximumPoolSize:最大執行緒數量
  • long keepAliveTime:非核心執行緒存活時間
  • TimeUnit unit:非核心執行緒存活時間的單位
  • BlockingQueue<Runnable> workQueue:阻塞佇列
  • ThreadFactory threadFactory:執行緒工廠
  • RejectedExecutionHandler handler:拒絕策略

雖然引數眾多,但我們關注的重點在 corePoolSize 和 maximumPoolSize 這兩個引數上。

通常在 corePoolSize 的設定上,會將程式區分為 IO 密集型和 CPU 密集型進行 corePoolSize 的設定:

  • CPU 密集型:\(CPU核心數+1\)
  • IO 密集型:\(CPU核心數\times 2\)

在 IO 密集型程式的 corePoolSize 設定上,還有另一種方案:\((\frac{執行緒等待時間}{執行緒執行時間} + 1)\times CPU核心數\)

例如,假設每個執行緒的執行時間為 1 秒,而執行緒等待獲取 CPU 的時間為 2 秒,CPU 核心數為 16,那麼根據以上公式可以得到:\((\frac{2}{1}+1)\times16=48\),即將 corePoolSize 設定為 48.

IO 密集型程式允許設定較大的 corePoolSize 的原因是,CPU 可能有大量時間都在等待 IO 操作而處於空閒狀態,設定較大的 corePoolSize 可以提升 CPU 的利用率,但這麼做的另一個問題是,IO 操作的速度是遠低於 CPU 的計算速度的, 程式的效能瓶頸會暴露在“低效”的 IO 操作上

除了以上兩種方案外,美團在Java執行緒池實現原理及其在美團業務中的實踐中也提到了一些 corePoolSize 和 maximumPoolSize 的設定方案:

那麼以上的計算方案哪個才是執行緒池設定的“銀劍”呢?實際上,以上的方案都只能應對一些特定的場景,而程式往往是複雜多變,且不可預估的,沒有哪一種理論方案可以應對所有的場景。

在設定執行緒池時,會根據以上理論公式預估出較為合理的初始設定,隨後透過壓測來調整執行緒池在極端場景的合理設定。當然,程式不會一直處於這種極端場景,如果有能力實現執行緒池的監控,可以根據實時情況調整執行緒池,保證程式執行的穩定性。

ThreadPoolExecutor 提供了一些方法,可以用於監控並動態調整執行緒池:

public class ThreadPoolExecutor extends AbstractExecutorService {
	// 設定執行緒工廠
	public void setThreadFactory(ThreadFactory threadFactory) {}

	// 設定聚聚策略
	public void setRejectedExecutionHandler(RejectedExecutionHandler handler) {}

	// 設定核心執行緒數
	public void setCorePoolSize(int corePoolSize) {}

	// 設定最大執行緒數
	public void setMaximumPoolSize(int maximumPoolSize) {}

	// 設定非核心執行緒存活時間
	public void setKeepAliveTime(long time, TimeUnit unit) {}

	// 獲取正在執行任務的大致執行緒數量
	public int getActiveCount() {}

	// 獲取執行緒池中曾經出現過的最大的活躍執行緒數
	public int getLargestPoolSize() {}

	// 獲取已經完成執行和待執行的大致任務數量
	public long getTaskCount() {}

	// 獲取已經完成執行的大致任務數量
	public long getCompletedTaskCount() {}

}

藉助以上方法,並新增對執行執行緒的監控,可以完成執行緒池的動態調整,以達到執行緒池在任何場景下都處於最佳設定中。

參考資料


如果本文對你有幫助的話,還請多多點贊支援。如果文章中出現任何錯誤,還請批評指正。最後歡迎大家關注分享硬核Java技術的金融摸魚俠王有志,我們下次再見!

相關文章