Java面試經典題:線程池專題

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

一、什麼是線程池

線程池的基本思想是一種對象池,在程序啓動時就開闢一塊內存空間,裏面存放了衆多(未死亡)的線程,池中線程執行調度由池管理器來處理。當有線程任務時,從池中取一個,執行完成後線程對象歸池,這樣能夠避免反覆建立線程對象所帶來的性能開銷,節省了系統的資源。git

二、使用線程池的好處

  1. 減小了建立和銷燬線程的次數,每一個工做線程均可以被重複利用,可執行多個任務。
  2. 運用線程池能有效的控制線程最大併發數,能夠根據系統的承受能力,調整線程池中工做線線程的數目,防止由於消耗過多的內存,而把服務器累趴下(每一個線程須要大約1MB內存,線程開的越多,消耗的內存也就越大,最後死機)。
  3. 對線程進行一些簡單的管理,好比:延時執行、定時循環執行的策略等,運用線程池都能進行很好的實現

三、線程池的主要組件

一個線程池包括如下四個基本組成部分:github

  1. 線程池管理器(ThreadPool):用於建立並管理線程池,包括 建立線程池,銷燬線程池,添加新任務;
  2. 工做線程(WorkThread):線程池中線程,在沒有任務時處於等待狀態,能夠循環的執行任務;
  3. 任務接口(Task):每一個任務必須實現的接口,以供工做線程調度任務的執行,它主要規定了任務的入口,任務執行完後的收尾工做,任務的執行狀態等;
  4. 任務隊列(taskQueue):用於存放沒有處理的任務。提供一種緩衝機制。

四、ThreadPoolExecutor類

講到線程池,要重點介紹java.uitl.concurrent.ThreadPoolExecutor類,ThreadPoolExecutor線程池中最核心的一個類,ThreadPoolExecutor在JDK中線程池經常使用類UML類關係圖以下:面試

咱們能夠經過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()方法來關閉線程池,不過它們也有所不一樣bash

  • shutdown的原理是隻是將線程池的狀態設置成SHUTDOWN狀態,而後中斷全部沒有正在執行任務的線程。
  • shutdownNow的原理是遍歷線程池中的工做線程,而後逐個調用線程的interrupt方法來中斷線程,因此沒法響應中斷的任務可能永遠沒法終止。shutdownNow會首先將線程池的狀態設置成STOP,而後嘗試中止全部的正在執行或暫停任務的線程,並返回等待執行任務的列表。

4. ThreadPoolExecutor執行的策略

/** * Executes the given task sometime in the future. The task * may execute in a new thread or in an existing pooled thread. * * If the task cannot be submitted for execution, either because this * executor has been shutdown or because its capacity has been reached, * the task is handled by the current {@code RejectedExecutionHandler}. * * @param command the task to execute * @throws RejectedExecutionException at discretion of * {@code RejectedExecutionHandler}, if the task * cannot be accepted for execution * @throws NullPointerException if {@code command} is null */
    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        /* * 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. * 若是當前的線程數小於核心線程池的大小,根據現有的線程做爲第一個Worker運行的線程, * 新建一個Worker,addWorker自動的檢查當前線程池的狀態和Worker的數量, * 防止線程池在不能添加線程的狀態下添加線程 * * 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. * 若是線程入隊成功,而後仍是要進行double-check的,由於線程池在入隊以後狀態是可能會發生變化的 * * 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. * * 若是task不能入隊(隊列滿了),這時候嘗試增長一個新線程,若是增長失敗那麼當前的線程池狀態變化了或者線程池已經滿了 * 而後拒絕task */
        int c = ctl.get();
        //當前的Worker的數量小於核心線程池大小時,新建一個Worker。
        if (workerCountOf(c) < corePoolSize) { 
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        //若是當前CorePool內的線程大於等於CorePoolSize,那麼將線程加入到BlockingQueue。
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))//recheck防止線程池狀態的突變,若是突變,那麼將reject線程,防止workQueue中增長新線程
                reject(command);
            else if (workerCountOf(recheck) == 0)//上下兩個操做都有addWorker的操做,可是若是在workQueue.offer的時候Worker變爲0,
              //那麼將沒有Worker執行新的task,因此增長一個Worker. addWorker(null, false);
        }
        //若是workQueue滿了,那麼這時候可能還沒到線程池的maxnum,因此嘗試增長一個Worker
        else if (!addWorker(command, false))
            reject(command);//若是Worker數量到達上限,那麼就拒絕此線程
    }
複製代碼

  1. 線程數量未達到corePoolSize,則新建一個線程(核心線程)執行任務
  2. 線程數量達到了corePools,則將任務移入隊列等待
  3. 隊列已滿,新建線程(非核心線程)執行任務
  4. 隊列已滿,總線程數又達到了maximumPoolSize,就會由(RejectedExecutionHandler)拋出異常

新建線程 -> 達到核心數 -> 加入隊列 -> 新建線程(非核心) -> 達到最大數 -> 觸發拒絕策略服務器

5. 四種拒絕策略

  1. AbortPolicy:不執行新任務,直接拋出異常,提示線程池已滿,線程池默認策略
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
        }
複製代碼
  1. DiscardPolicy:不執行新任務,也不拋出異常,基本上爲靜默模式。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        }
複製代碼
  1. DisCardOldSetPolicy:將消息隊列中的第一個任務替換爲當前新進來的任務執行
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                e.getQueue().poll();
                e.execute(r);
            }
        }
複製代碼
  1. CallerRunPolicy:用於被拒絕任務的處理程序,它直接在 execute 方法的調用線程中運行被拒絕的任務;若是執行程序已關閉,則會丟棄該任務。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                r.run();
            }
        }
複製代碼

五、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);
				}
			});
		}
	}
}
複製代碼

2. newFixedThreadPool

newFixedThreadPool建立一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待,指定線程池中的線程數量和最大線程數量同樣,也就線程數量固定不變ide

示例代碼以下函數

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();
					}
				}
			});
		}
	}
}
複製代碼

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秒執行一次
	}
}
複製代碼

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自帶的監控工具來監控咱們建立的線程數量,運行一個不終止的線程,建立指定量的線程,來觀察


六、線程池參數設置

參數的設置跟系統的負載有直接的關係,下面爲系統負載的相關參數:

  • tasks,每秒須要處理的的任務數(針對系統需求)
  • threadtasks,每一個線程每鈔可處理任務數(針對線程自己)
  • responsetime,系統容許任務最大的響應時間,好比每一個任務的響應時間不得超過2秒。

corePoolSize

系統每秒有tasks個任務須要處理理,則每一個線程每鈔可處理threadtasks個任務。,則須要的線程數爲:tasks/threadtasks,即tasks/threadtasks個線程數。

假設系統每秒任務數爲100 ~ 1000,每一個線程每鈔可處理10個任務,則須要100 / 10至1000 / 10,即10 ~ 100個線程。那麼corePoolSize應該設置爲大於10,具體數字最好根據8020原則,由於系統每秒任務數爲100 ~ 1000,即80%狀況下系統每秒任務數小於1000 * 20% = 200,則corePoolSize可設置爲200 / 10 = 20。

queueCapacity

任務隊列的長度要根據核心線程數,以及系統對任務響應時間的要求有關。隊列長度能夠設置爲 全部核心線程每秒處理任務數 * 每一個任務響應時間 = 每秒任務總響應時間 ,即(corePoolSize*threadtasks)responsetime: (2010)*2=400,即隊列長度可設置爲400。

maxPoolSize

當系統負載達到最大值時,核心線程數已沒法按時處理完全部任務,這時就須要增長線程。每秒200個任務須要20個線程,那麼當每秒達到1000個任務時,則須要(tasks - queueCapacity)/ threadtasks 即(1000-400)/10,即60個線程,可將maxPoolSize設置爲60。

隊列長度設置過大,會致使任務響應時間過長,切忌如下寫法:

LinkedBlockingQueue queue = new LinkedBlockingQueue();
複製代碼

這其實是將隊列長度設置爲Integer.MAX_VALUE,將會致使線程數量永遠爲corePoolSize,不再會增長,當任務數量陡增時,任務響應時間也將隨之陡增。

keepAliveTime

當負載下降時,可減小線程數量,當線程的空閒時間超過keepAliveTime,會自動釋放線程資源。默認狀況下線程池中止多餘的線程並最少會保持corePoolSize個線程。

allowCoreThreadTimeout

默認狀況下核心線程不會退出,可經過將該參數設置爲true,讓核心線程也退出。

七、線程池的五種狀態

  1. 線程池的初始化狀態是RUNNING,可以接收新任務,以及對已添加的任務進行處理。
  2. 線程池處在SHUTDOWN狀態時,不接收新任務,但能處理已添加的任務。 調用線程池的shutdown()接口時,線程池由RUNNING -> SHUTDOWN。
  3. 線程池處在STOP狀態時,不接收新任務,不處理已添加的任務,而且會中斷正在處理的任務。 調用線程池的shutdownNow()接口時,線程池由(RUNNING or SHUTDOWN ) -> STOP。
  4. 當全部的任務已終止,ctl記錄的」任務數量」爲0,線程池會變爲TIDYING狀態。當線程池變爲TIDYING狀態時,會執行鉤子函數terminated()。terminated()在ThreadPoolExecutor類中是空的,若用戶想在線程池變爲TIDYING時,進行相應的處理;能夠經過重載terminated()函數來實現。
  5. 當線程池在SHUTDOWN狀態下,阻塞隊列爲空而且線程池中執行的任務也爲空時,就會由 SHUTDOWN -> TIDYING。
  6. 當線程池在STOP狀態下,線程池中執行的任務爲空時,就會由STOP -> TIDYING。 線程池完全終止,就變成TERMINATED狀態。線程池處在TIDYING狀態時,執行完terminated()以後,就會由 TIDYING -> TERMINATED。

八、關閉線程池

線程池提供兩種關閉線程池方法:shutDown()和shutdownNow()

shutDown()

當線程池調用該方法時,線程池的狀態則馬上變成SHUTDOWN狀態。此時,則不能再往線程池中添加任何任務,不然將會拋出RejectedExecutionException異常。可是,此時線程池不會馬上退出,直到添加到線程池中的任務都已經處理完成,纔會退出。

shutdownNow()

根據JDK文檔描述,大體意思是:執行該方法,線程池的狀態馬上變成STOP狀態,並試圖中止全部正在執行的線程,再也不處理還在池隊列中等待的任務,固然,它會返回那些未執行的任務。

它試圖終止線程的方法是經過調用Thread.interrupt()方法來實現的,可是你們知道,這種方法的做用有限,若是線程中沒有sleep、wait、Condition、定時鎖等應用, interrupt()方法是沒法中斷當前的線程的。因此,ShutdownNow()並不表明線程池就必定當即就能退出,它可能必需要等待全部正在執行的任務都執行完成了才能退出。

相關文章
相關標籤/搜索