摘要 線程池的工作原理 構造方法 存活時間keepAliveTime 存活的時間單位uunit 任務隊列workQueue 線程工廠threadFactory 拒絕策略handler 任務執行的基本流程 向線程池提交任務 execute submit 關閉線程池 Executors架構 newCachedThreadPool造成的oom newSingleThreadExecutor造成的死結 閉鎖 訊號量 柵欄
摘要
說起進程大家都不陌生,學過作業系統的肯定都知道。而為了不使進程由於某種不當操作而陷入阻塞,進程就出現了。在一個進程中可以建立線程,線程是比進程更小的一個粒度。我們都知道通過繼承Thread類,或者實現runnable介面來建立一個線程,然而在進程中建立和銷毀線程都會消耗大量的時間,而且大量的子線程會分享主線程的系統資源,從而會使主線程因資源受限而導致應用效能降低為了減少線程在建立和銷毀的過程中所消耗的時間,為瞭解決這些問題,線程池就由此而生。 線程池的工作原理
首先看一個例子:
假設在一台伺服器完成一項任務的時間為T
T1 建立線程的時間
T2 線上程中執行任務的時間,包括線程間同步所需時間
T3 線程銷毀的時間
那麼T = T1+T2+T3。
分析一下:
T1,T3是多線程本身的帶來的開銷,希望減少T1,T3所用的時間,從而減少T的時間。如果在程式中頻繁的建立或銷毀線程,這導致T1和T3在T中佔有相當比例。顯然這是突出了線程的弱點(T1,T3),而不是優點(並發性)。線程池技術正是關注如何縮短或調整T1,T3時間的技術,從而提高伺服器程式效能的
並且線程的建立並不僅僅是我們new一個Thread那麼簡單。當JVM把建立線程的任務交給OS時,OS建立一個線程的代價是非常昂貴的,它需要給這個線程分配記憶體,列入調度,線上程切換的時候還要進行記憶體換頁,清空cpu緩衝,切換回來的時候還要重新從記憶體中讀取資訊,破壞資料的局部性。
然後就看一下我們的線程池吧
在java.util.concurrent包中,有這樣一個類ThreadPoolExecutor,查看他的繼承關係我們可以發現
Executor:介面 線程池必須要實現的介面 裡面只有一個方法 就是execute方法 。提交任務 ExecutorService:介面 繼承Executor介面 增加了submit提交任務的方式(有傳回值) 以及關閉 操作 AbstractExecutorService實現了ExecutorService的所有方法
ThreadPoolExecutor就是我們使用的線程池類了
而我們通常建立一個線程池會使用,Executors的靜態方法來建立幾個常用的線程池。而這幾個常用的線程池基本都是通過建立一個ThreadPoolExecutor方法實現的。
所以在介紹使用Executors線程池之前,先介紹一個ThreadPoolExecutor的構造方法 構造方法
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
| 變數 |
意義 |
| corePoolSize |
核心線程池大小 |
| maximumPoolSize |
最大線程池大小 |
| keepAliveTime |
空閑線程的存活時間 |
| unit |
空閑線程保持存活的時間的單位 |
| workQueue |
阻塞任務隊列,當要執行的任務超出corePoolSize ,那麼此時將把任務放入隊列中。 |
| threadFactory |
建立線程使用的工廠 |
| handler |
提交任務超出maxmumPoolSize+workQueue時的拒絕策略 |
存活時間(keepAliveTime)
keepAliveTime即空閑線程(大於corePoolSize 小於maximumPoolSize 的線程)保持存活的時間,超出這個時間,線程將被銷毀。如果設定了allowCoreThreadTimeOut(true),那麼核心線程在超出空閑時間時也會被銷毀 存活的時間單位u(unit)
空閑線程存活的時間單位,可選擇的有
TimeUnit.NANOSECONDS // 千分之一微秒(納秒)TimeUnit.MICROSECONDS // 千分之一毫秒(微妙)TimeUnit.MILLISECONDS // 千分之一秒(毫秒)TimeUnit.SECONDS // 秒 TimeUnit.MINUTES // 分TimeUnit.HOURS // 小時TimeUnit.DAYS // 天
任務隊列(workQueue) ArrayBlockingQueue:是一個基於數組結構的有界阻塞隊列,此隊列按 FIFO(先進先出)原則對元素進行排序。 LinkedBlockingQueue:一個基於鏈表結構的阻塞隊列,此隊列按 FIFO (先進先出) 排序元素,輸送量通常要高於 ArrayBlockingQueue。靜態Factory 方法Executors.newFixedThreadPool() 使用了這個隊列。 SynchronousQueue:一個不儲存元素的阻塞隊列。每個插入操作必須等到另一個線程調用移除操作,否則插入操作一直處於阻塞狀態,輸送量通常要高於LinkedBlockingQueue,靜態Factory 方法Executors.newCachedThreadPool 使用了這個隊列。 PriorityBlockingQueue:一個具有優先順序的無限阻塞隊列。 -
線程工廠(threadFactory)
每當線程需要建立一個線程時,都是通過線程Factory 方法。預設的線程Factory 方法將建立一個新的、非守護的線程,並且不包含特殊的配置資訊。通過定製一個線程工廠,可以定製線程池的配置資訊。比如當你希望為線程池中的線程維護一些統計資訊(包含有多少個線程被建立和銷毀)以及線上程被建立或者終止時調試訊息寫入日誌,都可以自訂線程工廠。 拒絕策略(handler)
當有界隊列被填滿後,拒絕策略開始發揮作用、JDK提供了集中不同的RejectedExecutionHandler 實現,每種實現都有不同的飽和策略。 AbortPolicy 中止策略 :預設的飽和策略,該策略將拋出為檢查的RejectedExecutionException.調用者可以捕獲這個異常,然後根據需求編寫自己的代碼。 DiscardPolicy 拋棄策略: 當新提交的任務無法儲存到隊列中等待執行時,拋棄策略會悄悄拋棄該任務 DiscardOldestPolicy 拋棄最舊的策略: 拋棄下一個將被執行的任務,然後添加新提交的任務 CallerRunsPolicy 調用者運行策略: 該策略實現了一種調用機制,該策略既不拋棄任務,也不拋出異常,而是將某些任務回退到調用者,從而降低新任務的流量。它不會線上程池的某個線程中執行新提交的任務,而是在一個調用了execute的線程中執行該任務(一般該調用者是main線程) 任務執行的基本流程
查看源碼後我們可以發現線程池任務的執行流程: 當線程池中的線程數小於corePoolSize 時,新提交的任務直接建立一個線程執行任務(不管是否有空閑線程) 當線程池中的線程數等於corePoolSize 時,新提交的任務將會進入阻塞隊列(workQueue)中,等待線程的調度 當阻塞隊列滿了以後,如果corePoolSize < maximumPoolSize ,則新提交的任務會建立線程執行任務,直至線程數達到maximumPoolSize
當線程數達到maximumPoolSize 時,新提交的任務會由(飽和策略)管理
在這裡簡單的測試一下CallerRunsPolicy 策略
我們建立了一個核心線程大小和最大線程大小都為1的線程池,然後設定其拒絕策略為CallerRunsPolicy,然後輸出任務執行的線程名稱。
/** * * @author xiaosuda * @date 2018/1/8 */public class CallerRunsPolicyTest { public static void main(String [] args){ ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS, new LinkedBlockingDeque<>(1), new ThreadPoolExecutor.CallerRunsPolicy()); for (int i = 0; i < 3; i++) { threadPoolExecutor.execute(() -> { try { System.out.println(Thread.currentThread().getName()); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }); } threadPoolExecutor.shutdown(); }}
輸出結果:
pool-1-thread-1
main
pool-1-thread-1
分析一下:因為核心線程池+ 阻塞隊列的大小=2,而我們建立了3個任務。當提交第一個任務時,建立線程執行任務,當提交第二個任務時,由於線程池的核心大小為1並且有任務在執行,放入阻塞隊列,當提交第三個任務,發現阻塞隊列已經滿了,而且線程池的最大的線程個數也達到了最大。所以交給拒絕策略來處理,而CallerRunsPolicy拒絕策略會將任務回退到調用者(main線程),從而降低新任務的流量。 向線程池提交任務 execute
我們可以使用 void execute(Runnable command)方法向線程池提交任務,但是該方法沒有傳回值
ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); cachedThreadPool.execute(new Runnable() { @Override public void run() { //do some things } });
submit
我們也可以使用 Future submit(Callable task)方法來提交任務。它會返回一個future對象,我們可以通過future.get來獲得傳回值,get方法會阻塞直到線程執行成功或者拋出異常。我們也可以使用 get(long timeout, TimeUnit unit) 來阻塞一段時間後立即返回,這時候可能任務還沒有執行成功。使用情境:比如我們在使用高德地圖來尋找去某個地方的路線時(路線可能有多種,但是在計算路線時耗費的時間不同),我們可以設定一個逾時時間,只顯示那些已經計算完成的路線。
舉個例子,我們要得到一個100以內的隨機數。
ExecutorService executorService = Executors.newSingleThreadExecutor(); Future<Integer> randomNum = executorService.submit(new Callable<Integer>() { @Override public Integer call() throws Exception { return new Random().nextInt(100); } }); try { System.out.println(randomNum.get()); } catch (InterruptedException e) { //中斷異常 e.printStackTrace(); } catch (ExecutionException e) { // 任務異常 e.printStackTrace(); } finally { //關閉線程池 executorService.shutdown(); }
關閉線程池
在ThreadPoolExecutor 中提供了關閉線程池的兩種方法。 shutdown shutdownNow
查看源碼後我們可以發現
shutdown:將線程池的狀態修改為SHUTDOWN,此時無法向線程池中新增工作,否則會由拒絕策略來處理添加的任務。但是已經添加的任務(任務隊列中的任務和正在執行的任務)會繼續執行直到完成,然後線程池退出。
shutdownNow:將線程池的狀態修改為STOP,此時無法向線程池新增工作,否則會由拒絕策略來處理添加的任務。並且任務隊列中的任務也不再執行,只執行那些正在執行的任務,並且試圖中斷它們,然後返回那些未執行的任務,線程池退出。
/** * * @author xiaosuda * @date 2018/1/8 */public class ShutdownNowTest { public static void main(String [] args){ Integer threadNum = Runtime.getRuntime().availableProcessors(); ThreadFactoryBuilder threadFactoryBuilder = new ThreadFactoryBuilder(); threadFactoryBuilder.setNameFormat("ShutdownNowTestPool"); ThreadFactory threadFactory = threadFactoryBuilder.build(); ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, threadNum, 60L, TimeUnit.SECONDS, new LinkedBlockingDeque<>(5), threadFactory, new ThreadPoolExecutor.AbortPolicy()); for (int i = 0; i < 10; i++) { String content = "Thread-name" + i; threadPoolExecutor.execute(new Runnable() { @Override public void run() { Thread.currentThread().setName(content); int x = 10000; //為了類比長時間任務,避免任務隊列中沒有任務 while (x-- > 0) { } System.out.println( Thread.currentThread().getName()); } }); } List<Runnable> runnables = threadPoolExecutor.shutdownNow(); System.out.println("--------------------------------未執行的任務--------------------------------"); for (Runnable runnable : runnables) { new Thread(runnable).start(); } }} 執行結果:
Thread-name0
Thread-name6
Thread-name1
Thread-name2
Thread-name3
Thread-name7
——————————–未執行的任務——————————–
Thread-name8
Thread-name4
Thread-name5
Thread-name9 Executors架構 在Executors,jdk為我們提供了幾種常用的線程池 newFixedThreadPool:此時核心線程池數量等於線程池最大數量。 將建立一個固定長度的線程池,阻塞隊列使用的是LinkedBlockingQueue,線程可以重用。 newCachedThreadPool: 建立一個可以緩衝的線程池,預設的核心線程池數量為0,最大線程池數量為Integer.MAX_VALUE.阻塞隊列使用的是SynchronousQueue。線程的存活時間為60s.一旦有任務提交就建立一個線程執行任務,如果有空閑線程超出60s自動回收,當需求大量增加,且任務的執行時間較長時,容易oom,此時可以使用訊號量來控制同時執行線程的個數。 newSingleThreadExecutor: 建立一個核心線程和最大線程數都為1的線程池,阻塞隊列為LinkedBlockingQueue。串列執行任務,此時如果線上程中為線程池新增工作要注意避免死結的情況發生。 newScheduledThreadPool:建立一個固定長度的線程池,而且以延遲或者定時的方式執行任務。 newCachedThreadPool造成的oom
建立了一個CachedThreadPool,新增工作時睡眠100毫秒,以到達執行長時間任務的目地。迴圈地向CachedThreadPool中新增工作。
/** * * @author xiaosuda * @date 2018/1/3 */public class TestExecutorsThread { public static void main(String [] args){ cacheThreadPool(); } private static void cacheThreadPool() { ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); for (int i = 0; i < Integer.MAX_VALUE; i++) { cachedThreadPool.execute(() ->{ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } }); } cachedThreadPool.shutdown(); }}
執行結果後發現OOM:
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread at java.lang.Thread.start0(Native Method) at java.lang.Thread.start(Thread.java:717) at java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:950) at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1368) at com.dfire.Thread.TestExecutorsThread.cacheThreadPool(TestExecutorsThread.java:19) at com.dfire.Thread.TestExecutorsThread.main(TestExecutorsThread.java:14)
所以我們在使用newCachedThreadPool時應該注意避免添加長時間的任務,或者限制並發執行的任務的數量。 newSingleThreadExecutor造成的死結
由於newSingleThreadExecutor建立的是單一的線程,任務都在串列執行,如果任務間產生依賴,那麼就容易發生死結。下面舉個newSingleThreadExecutor死結例子。
/** * * @author xiaosuda * @date 2018/1/8 */public class SingleThreadExecutorTest { public static void main(String [] args) throws ExecutionException, InterruptedException { ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(); Random random = new Random(); Future<Integer> randomSum = singleThreadExecutor.submit(new Callable<Integer>() { @Override public Integer call() throws Exception { Future<Integer> randomOne = singleThreadExecutor.submit(new Callable<Integer>() { @Override public Integer call() throws Exception { return random.nextInt(100); } }); Future<Integer> randomTwo = singleThreadExecutor.submit(new Callable<Integer>() { @Override public Integer call() throws Exception { return random.nextInt(100); } }); return randomOne.get() + randomTwo.get(); } }); System.out.println(randomSum.get()); }}
程式一直在執行,但沒有結果輸出,通過 jstack -l [pid] ->thread.jstack 命令分析
開啟thread.jstack後會發現:
2018-01-08 12:38:52Full thread dump Java HotSpot(TM) Client VM (25.131-b11 mixed mode):"pool-1-thread-1" #11 prio=5 os_prio=0 tid=0x14e47c00 nid=0x1278 waiting on condition [0x155ff000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x04750dd0> (a java.util.concurrent.FutureTask) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) at java.util.concurrent.FutureTask.awaitDone(FutureTask.java:429) at java.util.concurrent.FutureTask.get(FutureTask.java:191) at com.dfire.Thread.SingleThreadExecutorTest$1.call(SingleThreadExecutorTest.java:31) at com.dfire.Thread.SingleThreadExecutorTest$1.call(SingleThreadExecutorTest.java:16) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at java.lang.Thread.run(Thread.java:748) Locked ownable synchronizers: - <0x04742ca0> (a java.util.concurrent.ThreadPoolExecutor$Worker)
線程發生了死結。 閉鎖
閉鎖時一種同步工具類,可以延遲線程的進度直到達到終止狀態,閉鎖就相當於一扇門,在閉鎖到達結束狀態之前,這扇門一直時關閉的,並且沒有任何線程能通過。當閉鎖到達結束狀態後,將不會再改變狀態,因此這扇門將永遠保持開啟狀態,閉鎖可以用來確保某些活動知道其它活動都完成後才繼續執行。
CountDownLatch就是一種靈活的閉鎖實現。
比如:一個簡單的zookeeper串連。
/** * @author xiaosuda * @date 2018/01/08 */public class ConnectionWatcher implements Watcher { private static final int SESSION_TIMEOUT = 5000; protected ZooKeeper zooKeeper; private CountDownLatch connectedSignal = new CountDownLatch(1); public void connect(String hosts) throws IOException, InterruptedException { zooKeeper = new ZooKeeper(hosts, SESSION_TIMEOUT, this); connectedSignal.await(); } @Override public void process(WatchedEvent watchedEvent) { if (watchedEvent.getState() == Event.KeeperState.SyncConnected) { connectedSignal.countDown(); System.out.println("串連成功"); } } public void close() throws InterruptedException { zooKeeper.close(); System.out.println("串連關閉"); } public static void main(String [] args) throws IOException, InterruptedException, KeeperException { ConnectionWatcher connectionWatcher = new ConnectionWatcher(); connectionWatcher.connect("117.88.151.70:2181"); connectionWatcher.close(); }}
在這裡我們使用了CountDownLatch,並且計數器的初始值為1。在connect方法中,我們使用connectedSignal.await()進入等待狀態。ConnectionWatcher類實現了Watcher介面,別且重寫了process方法。在process方法中,如果串連成功就使計數器的值-1,將等待狀態變為結束。開始執行串連成功的後一些操作。
還有FutureTask也可以用做閉鎖(FutureTask實現了Future語義,表示一種抽象的可產生結果的計算)。Future.get的行為取決於任務的狀態,如果任務已經完成,那麼get會立即返回結果,否則將阻塞知道任務進入完成狀態,然後返回結果或者拋出異常。 訊號量
訊號量(Semaphore)用來控制同時訪問某個特定資源的運算元量,或者同時執行某定操作的數量,訊號量還可以用來實現某種資源集區,或者對容器施加邊界。
訊號量有兩個常用的方法:
Semaphore管理著一組虛擬許可,許可的初始狀態可以通過建構函式來執行,在執行操作時可以首先獲得許可,並在使用以後釋放許可。如果沒有許可,那麼semaphore.acquire()將阻塞直到有許可。semaphore.release()方法將返回一個許可訊號量。訊號量的一種簡化形式是二值訊號量,即初始值為1的semaphore。二值訊號量可以做互斥體,並不具備不可重新進入的加鎖語義,誰擁有這個唯一的許可,就擁有了互斥鎖。在上面的newCachedThreadPool造成的oom中,我們可以使用訊號量來控制任務的並發量。修改後的代碼如下:
/** * * @author xiaosuda * @date 2018/1/3 */public class TestExecutorsThread { private static Integer MAX_TASK = 100; private static Semaphore semaphore = new Semaphore(MAX_TASK); public static void main(String [] args) throws InterruptedException { cacheThreadPool(); } private static void cacheThreadPool() throws InterruptedException { ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); for (int i = 0; i < Integer.MAX_VALUE; i++) { semaphore.acquire(); cachedThreadPool.execute(() ->{ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } finally { semaphore.release(); } }); } cachedThreadPool.shutdown(); }}
執行後發現不會出現OOM,並且最終會全部執行成功。 柵欄
我們已經看到通過閉鎖來啟動一組相關的操作,或者等待一組相關的操作結束。閉鎖是一次性對象,一旦進入終止狀態,就不能被重設。柵欄類似於閉鎖,它能阻塞一組線程知道某個事件發生。柵欄與閉鎖的區別在於:所有線程必須同時到達柵欄位置,才能繼續執行。到線程到達柵欄位置時調用await方法即可。閉鎖用於等待事件,而柵欄用於等待線程。柵欄用於實現一些協議,例如:明天3年紀2班的所有學生8:00在學校門口集合,到了以後要等其他人,然後一起做下一步的事情,比如去博物館。
/** * * @author xiaosuda * @date 2018/1/8 */public class CyclicBarrierTest { private CyclicBarrier cyclicBarrier = null; private Exe