Java面試經典題:執行緒池專題

在雲端發表於2019-03-04

總結Java開發面試常問的問題,GitHub地址:github.com/zaiyunduan1…,持續更新中~,如果對你有幫助歡迎Star

1、什麼是執行緒池

執行緒池的基本思想是一種物件池,在程式啟動時就開闢一塊記憶體空間,裡面存放了眾多(未死亡)的執行緒,池中執行緒執行排程由池管理器來處理。當有執行緒任務時,從池中取一個,執行完成後執行緒物件歸池,這樣可以避免反覆建立執行緒物件所帶來的效能開銷,節省了系統的資源。

2、使用執行緒池的好處

  1. 減少了建立和銷燬執行緒的次數,每個工作執行緒都可以被重複利用,可執行多個任務。
  2. 運用執行緒池能有效的控制執行緒最大併發數,可以根據系統的承受能力,調整執行緒池中工作線執行緒的數目,防止因為消耗過多的記憶體,而把伺服器累趴下(每個執行緒需要大約1MB記憶體,執行緒開的越多,消耗的記憶體也就越大,最後當機)。
  3. 對執行緒進行一些簡單的管理,比如:延時執行、定時迴圈執行的策略等,運用執行緒池都能進行很好的實現

3、執行緒池的主要元件

Java面試經典題:執行緒池專題

一個執行緒池包括以下四個基本組成部分:

  1. 執行緒池管理器(ThreadPool):用於建立並管理執行緒池,包括 建立執行緒池,銷燬執行緒池,新增新任務;
  2. 工作執行緒(WorkThread):執行緒池中執行緒,在沒有任務時處於等待狀態,可以迴圈的執行任務;
  3. 任務介面(Task):每個任務必須實現的介面,以供工作執行緒排程任務的執行,它主要規定了任務的入口,任務執行完後的收尾工作,任務的執行狀態等;
  4. 任務佇列(taskQueue):用於存放沒有處理的任務。提供一種緩衝機制。

4、ThreadPoolExecutor類

講到執行緒池,要重點介紹java.uitl.concurrent.ThreadPoolExecutor類,ThreadPoolExecutor執行緒池中最核心的一個類,ThreadPoolExecutor在JDK中執行緒池常用類UML類關係圖如下:

Java面試經典題:執行緒池專題
我們可以通過ThreadPoolExecutor來建立一個執行緒池

new ThreadPoolExecutor(corePoolSize, maximumPoolSize,keepAliveTime, 
milliseconds,runnableTaskQueue, threadFactory,handler);
複製程式碼

1. 建立一個執行緒池需要輸入幾個引數

  • corePoolSize(執行緒池的基本大小):當提交一個任務到執行緒池時,執行緒池會建立一個執行緒來執行任務,即使其他空閒的基本執行緒能夠執行新任務也會建立執行緒,等到需要執行的任務數大於執行緒池基本大小時就不再建立。如果呼叫了執行緒池的prestartAllCoreThreads方法,執行緒池會提前建立並啟動所有基本執行緒。
  • maximumPoolSize(執行緒池最大大小):執行緒池允許建立的最大執行緒數。如果佇列滿了,並且已建立的執行緒數小於最大執行緒數,則執行緒池會再建立新的執行緒執行任務。值得注意的是如果使用了無界的任務佇列這個引數就沒什麼效果。
  • runnableTaskQueue(任務佇列):用於儲存等待執行的任務的阻塞佇列。
  • ThreadFactory:用於設定建立執行緒的工廠,可以通過執行緒工廠給每個建立出來的執行緒設定更有意義的名字,Debug和定位問題時非常又幫助。
  • RejectedExecutionHandler(拒絕策略):當佇列和執行緒池都滿了,說明執行緒池處於飽和狀態,那麼必須採取一種策略處理提交的新任務。這個策略預設情況下是AbortPolicy,表示無法處理新任務時丟擲異常。以下是JDK1.5提供的四種策略。n AbortPolicy:直接丟擲異常。
  • keepAliveTime(執行緒活動保持時間):執行緒池的工作執行緒空閒後,保持存活的時間。所以如果任務很多,並且每個任務執行的時間比較短,可以調大這個時間,提高執行緒的利用率。
  • TimeUnit(執行緒活動保持時間的單位):可選的單位有天(DAYS),小時(HOURS),分鐘(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。

2. 向執行緒池提交任務

我們可以通過execute()或submit()兩個方法向執行緒池提交任務,不過它們有所不同

  • execute()方法沒有返回值,所以無法判斷任務知否被執行緒池執行成功
threadsPool.execute(new Runnable() {
    @Override
    public void run() {
    // TODO Auto-generated method stub
   }
});
複製程式碼
  • submit()方法返回一個future,那麼我們可以通過這個future來判斷任務是否執行成功,通過future的get方法來獲取返回值
try {
     Object s = future.get();
   } catch (InterruptedException e) {
   // 處理中斷異常
   } catch (ExecutionException e) {
   // 處理無法執行任務異常
   } finally {
   // 關閉執行緒池
   executor.shutdown();
}
複製程式碼

3. 執行緒池的關閉

我們可以通過shutdown()或shutdownNow()方法來關閉執行緒池,不過它們也有所不同

  • shutdown的原理是隻是將執行緒池的狀態設定成SHUTDOWN狀態,然後中斷所有沒有正在執行任務的執行緒。
  • shutdownNow的原理是遍歷執行緒池中的工作執行緒,然後逐個呼叫執行緒的interrupt方法來中斷執行緒,所以無法響應中斷的任務可能永遠無法終止。shutdownNow會首先將執行緒池的狀態設定成STOP,然後嘗試停止所有的正在執行或暫停任務的執行緒,並返回等待執行任務的列表。

4. ThreadPoolExecutor執行的策略

Java面試經典題:執行緒池專題

  1. 執行緒數量未達到corePoolSize,則新建一個執行緒(核心執行緒)執行任務
  2. 執行緒數量達到了corePools,則將任務移入佇列等待
  3. 佇列已滿,新建執行緒(非核心執行緒)執行任務
  4. 佇列已滿,匯流排程數又達到了maximumPoolSize,就會由(RejectedExecutionHandler)丟擲異常

新建執行緒 -> 達到核心數 -> 加入佇列 -> 新建執行緒(非核心) -> 達到最大數 -> 觸發拒絕策略

5. 四種拒絕策略

  1. AbortPolicy:不執行新任務,直接丟擲異常,提示執行緒池已滿,執行緒池預設策略
  2. DiscardPolicy:不執行新任務,也不丟擲異常,基本上為靜默模式。
  3. DisCardOldSetPolicy:將訊息佇列中的第一個任務替換為當前新進來的任務執行
  4. CallerRunPolicy:拒絕新任務進入,如果該執行緒池還沒有被關閉,那麼這個新的任務在執行執行緒中被呼叫)

5、Java通過Executors提供四種執行緒池

  1. CachedThreadPool():可快取執行緒池。
  • 執行緒數無限制
  • 有空閒執行緒則複用空閒執行緒,若無空閒執行緒則新建執行緒 一定程式減少頻繁建立/銷燬執行緒,減少系統開銷
  1. FixedThreadPool():定長執行緒池。
  • 可控制執行緒最大併發數(同時執行的執行緒數)
  • 超出的執行緒會在佇列中等待
  1. ScheduledThreadPool():
  • 定時執行緒池。
  • 支援定時及週期性任務執行。
  1. SingleThreadExecutor():單執行緒化的執行緒池。
  • 有且僅有一個工作執行緒執行任務
  • 所有任務按照指定順序執行,即遵循佇列的入隊出隊規則

1. newCachedThreadPool

newCachedThreadPool建立一個可快取執行緒池,如果執行緒池長度超過處理需要,可靈活回收空閒執行緒,若無可回收,則新建執行緒

public class ThreadPoolExecutorTest1 {
	public static void main(String[] args) {
		ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
		for (int i = 0; i < 1000; i++) {
			final int index = i;
			try {
				Thread.sleep(index * 1000);
			} catch (Exception e) {
				e.printStackTrace();
			}
			cachedThreadPool.execute(new Runnable() {
				public void run() {
					System.out.println(Thread.currentThread().getName()+":"+index);
				}
			});
		}
	}
}
複製程式碼

Java面試經典題:執行緒池專題

2. newFixedThreadPool

newFixedThreadPool建立一個定長執行緒池,可控制執行緒最大併發數,超出的執行緒會在佇列中等待,指定執行緒池中的執行緒數量和最大執行緒數量一樣,也就執行緒數量固定不變

示例程式碼如下

public class ThreadPoolExecutorTest {
	public static void main(String[] args) {
		ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);// 每隔兩秒列印3個數
		for (int i = 0; i < 10; i++) {
			final int index = i;
			fixedThreadPool.execute(new Runnable() {
				public void run() {
					try {
						System.out.println(Thread.currentThread().getName()+":"+index);
						//三個執行緒併發
						Thread.sleep(2000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			});
		}
	}
}
複製程式碼

Java面試經典題:執行緒池專題

3. newscheduledThreadPool

newscheduledThreadPool建立一個定長執行緒池,支援定時及週期性任務執行。延遲執行示例程式碼如下.表示延遲1秒後每3秒執行一次

public class ThreadPoolExecutorTest3 {
	public static void main(String[] args) {
		ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
		scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
			public void run() {
				System.out.println(Thread.currentThread().getName() + ": delay 1 seconds, and excute every 3 seconds");
			}
		}, 1, 3, TimeUnit.SECONDS);// 表示延遲1秒後每3秒執行一次
	}
}
複製程式碼

Java面試經典題:執行緒池專題

4. newSingleThreadExecutor

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

public class ThreadPoolExecutorTest4 {
	public static void main(String[] args) {
		ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
		for (int i = 0; i < 10; i++) {
			final int index = i;
			singleThreadExecutor.execute(new Runnable() {
				public void run() {
					try {
						System.out.println(Thread.currentThread().getName() + ":" + index);
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			});
		}
	}
}
複製程式碼

結果依次輸出,相當於順序執行各個任務。使用JDK自帶的監控工具來監控我們建立的執行緒數量,執行一個不終止的執行緒,建立指定量的執行緒,來觀察

Java面試經典題:執行緒池專題


6、執行緒池引數設定

引數的設定跟系統的負載有直接的關係,下面為系統負載的相關引數:

  • tasks,每秒需要處理的的任務數
  • tasktime,處理每個任務花費的時間
  • responsetime,系統允許任務最大的響應時間,比如每個任務的響應時間不得超過2秒。

corePoolSize

每個任務需要tasktime秒處理,則每個執行緒每鈔可處理1/tasktime個任務。系統每秒有tasks個任務需要處理,則需要的執行緒數為:tasks/(1/tasktime),即tasks*tasktime個執行緒數。

假設系統每秒任務數為100 ~ 1000,每個任務耗時0.1秒,則需要100 * 0.1至1000 * 0.1,即10 ~ 100個執行緒。那麼corePoolSize應該設定為大於10,具體數字最好根據8020原則,即80%情況下系統每秒任務數小於200,最多時為1000,則corePoolSize可設定為20。

maxPoolSize

當系統負載達到最大值時,核心執行緒數已無法按時處理完所有任務,這時就需要增加執行緒。每秒200個任務需要20個執行緒,那麼當每秒達到1000個任務時,則需要(1000-queueCapacity)*(20/200),即60個執行緒,可將maxPoolSize設定為60。

queueCapacity

任務佇列的長度要根據核心執行緒數,以及系統對任務響應時間的要求有關。佇列長度可以設定為(corePoolSize/tasktime)*responsetime: (20/0.1)*2=400,即佇列長度可設定為400。

佇列長度設定過大,會導致任務響應時間過長,切忌以下寫法:

LinkedBlockingQueue queue = new LinkedBlockingQueue();
複製程式碼

這實際上是將佇列長度設定為Integer.MAX_VALUE,將會導致執行緒數量永遠為corePoolSize,再也不會增加,當任務數量陡增時,任務響應時間也將隨之陡增。

keepAliveTime

當負載降低時,可減少執行緒數量,當執行緒的空閒時間超過keepAliveTime,會自動釋放執行緒資源。預設情況下執行緒池停止多餘的執行緒並最少會保持corePoolSize個執行緒。

allowCoreThreadTimeout

預設情況下核心執行緒不會退出,可通過將該引數設定為true,讓核心執行緒也退出。

相關文章