Java的線程池實現從根本上來講只有兩個:ThreadPoolExecutor類和ScheduledThreadPoolExecutor類,這兩個類仍是父子關係,可是Java爲了簡化並行計算,還提供了一個Exceutors的靜態類,它能夠直接生成多種不一樣的線程池執行器,好比單線程執行器、帶緩衝功能的執行器等,但歸根結底仍是使用ThreadPoolExecutor類或ScheduledThreadPoolExecutor類的封裝類。編程
爲了理解這些執行器,咱們首先來看看ThreadPoolExecutor類,其中它複雜的構造函數能夠很好的理解線程池的做用,代碼以下: 安全
public class ThreadPoolExecutor extends AbstractExecutorService { // 最完整的構造函數 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最完整的構造函數,其餘的構造函數都是引用該構造函數實現的,咱們逐步來解釋這些參數的含義。多線程
線程池的管理是這樣一個過程:首先建立線程池,而後根據任務的數量逐步將線程增大到corePoolSize數量,若是此時仍有任務增長,則放置到workQuene中,直到workQuene爆滿爲止,而後繼續增長池中的數量(加強處理能力),最終達到maximumPoolSize,那若是此時還有任務增長進來呢?這就須要handler處理了,或者丟棄任務,或者拒絕新任務,或者擠佔已有任務等。併發
在任務隊列和線程池都飽和的狀況下,一但有線程處於等待(任務處理完畢,沒有新任務增長)狀態的時間超過keepAliveTime,則該線程終止,也就說池中的線程數量會逐漸下降,直至爲corePoolSize數量爲止。app
咱們能夠把線程池想象爲這樣一個場景:在一個生產線上,車間規定是能夠有corePoolSize數量的工人,可是生產線剛創建時,工做很少,不須要那麼多的人。隨着工做數量的增長,工人數量也逐漸增長,直至增長到corePoolSize數量爲止。此時還有任務增長怎麼辦呢?異步
好辦,任務排隊,corePoolSize數量的工人不停歇的處理任務,新增長的任務按照必定的規則存放在倉庫中(也就是咱們的workQuene中),一旦任務增長的速度超過了工人處理的能力,也就是說倉庫爆滿時,車間就會繼續招聘工人(也就是擴大線程數),直至工人數量到達maximumPoolSize爲止,那若是全部的maximumPoolSize工人都在處理任務時,並且倉庫也是飽和狀態,新增任務該怎麼處理呢?這就會扔一個叫handler的專門機構去處理了,它要麼丟棄這些新增的任務,要麼無視,要麼替換掉別的任務。ide
過了一段時間後,任務的數量逐漸減小,致使一部分工人處於待工狀態,爲了減小開支(Java是爲了減小系統的資源消耗),因而開始辭退工人,直至保持corePoolSize數量的工人爲止,此時即便沒有工做,也再也不辭退工人(池中的線程數量再也不減小),這也是保證之後再有任務時可以快速的處理。函數
明白了線程池的概念,咱們再來看看Executors提供的幾個線程建立線程池的便捷方法:性能
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
它的使用方法也很簡單,下面是簡單的示例:測試
public static void main(String[] args) throws ExecutionException, InterruptedException { // 建立單線程執行器 ExecutorService es = Executors.newSingleThreadExecutor(); // 執行一個任務 Future<String> future = es.submit(new Callable<String>() { @Override public String call() throws Exception { return ""; } }); // 得到任務執行後的返回值 System.out.println("返回值:" + future.get()); // 關閉執行器 es.shutdown(); }
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
這裏須要說明的是,任務隊列使用了同步阻塞隊列,這意味着向隊列中加入一個元素,便可喚醒一個線程(新建立的線程或複用空閒線程來處理),這種隊列已經沒有隊列深度的概念了.
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
上面返回的是一個ThreadPoolExecutor,它的corePoolSize和maximumPoolSize是相等的,也就是說,最大線程數量爲nThreads。若是任務增加的速度很是快,超過了LinkedBlockingQuene的最大容量(Integer的最大值),那此時會如何處理呢?會按照ThreadPoolExecutor默認的拒絕策略(默認是DiscardPolicy,直接丟棄)來處理。
以上三種線程池執行器都是ThreadPoolExecutor的簡化版,目的是幫助開發人員屏蔽過得線程細節,簡化多線程開發。當須要運行異步任務時,能夠直接經過Executors得到一個線程池,而後運行任務,不須要關注ThreadPoolExecutor的一系列參數是什麼含義。固然,有時候這三個線程不能知足要求,此時則能夠直接操做ThreadPoolExecutor來實現複雜的多線程計算。能夠這樣比喻,newSingleThreadExecutor、newCachedThreadPool、newFixedThreadPool是線程池的簡化版,而ThreadPoolExecutor則是旗艦版___簡化版容易操做,須要瞭解的知識相對少些,方便使用,而旗艦版功能齊全,適用面廣,難以駕馭。
不少編碼者都會說,Lock類和synchronized關鍵字用在代碼塊的併發性和內存上時語義是同樣的,都是保持代碼塊同時只有一個線程執行權。這樣的說法只說對了一半,咱們以一個任務提交給多個線程爲例,來看看使用顯示鎖(Lock類)和內部鎖(synchronized關鍵字)有什麼不一樣,首先定義一個任務:
class Task { public void doSomething() { try { // 每一個線程等待2秒鐘,注意此時線程的狀態轉變爲Warning狀態 Thread.sleep(2000); } catch (Exception e) { // 異常處理 } StringBuffer sb = new StringBuffer(); // 線程名稱 sb.append("線程名稱:" + Thread.currentThread().getName()); // 運行時間戳 sb.append(",執行時間: " + Calendar.getInstance().get(Calendar.SECOND) + "s"); System.out.println(sb); } }
該類模擬了一個執行時間比較長的計算,注意這裏是模擬方式,在使用sleep方法時線程的狀態會從運行狀態轉變爲等待狀態。該任務具有多線程能力時必須實現Runnable接口,咱們分別創建兩種不一樣的實現機制,先看顯示鎖實現:
class TaskWithLock extends Task implements Runnable { // 聲明顯示鎖 private final Lock lock = new ReentrantLock(); @Override public void run() { try { // 開始鎖定 lock.lock(); doSomething(); } finally { // 釋放鎖 lock.unlock(); } } }
這裏有一點須要說明,顯示鎖的鎖定和釋放必須放在一個try......finally塊中,這是爲了確保即便出現異常也能正常釋放鎖,保證其它線程能順利執行。
內部鎖的處理也很是簡單,代碼以下:
//內部鎖任務 class TaskWithSync extends Task implements Runnable{ @Override public void run() { //內部鎖 synchronized("A"){ doSomething(); } } }
這兩個任務看着很是類似,應該可以產生相同的結果吧?咱們創建一個模擬場景,保證同時有三個線程在運行,代碼以下:
public class Client127 { public static void main(String[] args) throws Exception { // 運行顯示任務 runTasks(TaskWithLock.class); // 運行內部鎖任務 runTasks(TaskWithSync.class); } public static void runTasks(Class<? extends Runnable> clz) throws Exception { ExecutorService es = Executors.newCachedThreadPool(); System.out.println("***開始執行 " + clz.getSimpleName() + " 任務***"); // 啓動3個線程 for (int i = 0; i < 3; i++) { es.submit(clz.newInstance()); } // 等待足夠長的時間,而後關閉執行器 TimeUnit.SECONDS.sleep(10); System.out.println("---" + clz.getSimpleName() + " 任務執行完畢---\n"); // 關閉執行器 es.shutdown(); } }
按照通常的理解,Lock和synchronized的處理方式是相同的,輸出應該沒有差異,可是很遺憾的是,輸出差異其實很大。輸出以下:
***開始執行 TaskWithLock 任務***
線程名稱:pool-1-thread-2,執行時間: 55s
線程名稱:pool-1-thread-1,執行時間: 55s
線程名稱:pool-1-thread-3,執行時間: 55s
---TaskWithLock 任務執行完畢---
***開始執行 TaskWithSync 任務***
線程名稱:pool-2-thread-1,執行時間: 5s
線程名稱:pool-2-thread-3,執行時間: 7s
線程名稱:pool-2-thread-2,執行時間: 9s
---TaskWithSync 任務執行完畢---
注意看運行的時間戳,顯示鎖是同時運行的,很顯然pool-1-thread-1線程執行到sleep時,其它兩個線程也會運行到這裏,一塊兒等待,而後一塊兒輸出,這還具備線程互斥的概念嗎?
而內部鎖的輸出則是咱們預期的結果,pool-2-thread-1線程在運行時其它線程處於等待狀態,pool-2-threda-1執行完畢後,JVM從等待線程池中隨機獲的一個線程pool-2-thread-3執行,最後執行pool-2-thread-2,這正是咱們但願的。
如今問題來了:Lock鎖爲何不出現互斥狀況呢?
這是由於對於同步資源來講(示例中的代碼塊)顯示鎖是對象級別的鎖,而內部鎖是類級別的鎖,也就說說Lock鎖是跟隨對象的,synchronized鎖是跟隨類的,更簡單的說把Lock定義爲多線程類的私有屬性是起不到資源互斥做用的,除非是把Lock定義爲全部線程的共享變量。都說代碼是最好的解釋語言,咱們來看一個Lock鎖資源的代碼:
public static void main(String[] args) { // 多個線程共享鎖 final Lock lock = new ReentrantLock(); // 啓動三個線程 for (int i = 0; i < 3; i++) { new Thread(new Runnable() { @Override public void run() { try { lock.lock(); // 休眠2秒鐘 Thread.sleep(2000); System.out.println(Thread.currentThread().getName()); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } }).start(); } }
執行時,會發現線程名稱Thread-0、Thread-一、Thread-2會逐漸輸出,也就是一個線程在執行時,其它線程就處於等待狀態。注意,這裏三個線程運行的實例對象是同一個類。
除了這一點不一樣以外,顯示鎖和內部鎖還有什麼區別呢?還有如下4點不一樣:
class Foo { // 可重入的讀寫鎖 private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); // 讀鎖 private final Lock r = rwl.readLock(); // 寫鎖 private final Lock w = rwl.writeLock(); // 多操做,可併發執行 public void read() { try { r.lock(); Thread.sleep(1000); System.out.println("read......"); } catch (InterruptedException e) { e.printStackTrace(); } finally { r.unlock(); } } // 寫操做,同時只容許一個寫操做 public void write() { try { w.lock(); Thread.sleep(1000); System.out.println("write....."); } catch (InterruptedException e) { e.printStackTrace(); } finally { w.unlock(); } } }
能夠編寫一個Runnable實現類,把Foo類做爲資源進行調用(注意多線程是共享這個資源的),而後就會發現這樣的現象:讀寫鎖容許同時有多個讀操做但只容許一個寫操做,也就是當有一個寫線程在執行時,全部的讀線程都會阻塞,直到寫線程釋放鎖資源爲止,而讀鎖則能夠有多個線程同時執行。
2.Lock鎖是無阻塞鎖,synchronized是阻塞鎖
當線程A持有鎖時,線程B也指望得到鎖,此時,若是程序中使用的顯示鎖,則B線程爲等待狀態(在一般的描述中,也認爲此線程被阻塞了),若使用的是內部鎖則爲阻塞狀態。
3.Lock可實現公平鎖,synchronized只能是非公平鎖
什麼叫非公平鎖呢?當一個線程A持有鎖,而線程B、C處於阻塞(或等待)狀態時,若線程A釋放鎖,JVM將從線程B、C中隨機選擇一個持有鎖並使其得到執行權,這叫非公平鎖(由於它拋棄了先來後到的順序);若JVM選擇了等待時間最長的一個線程持有鎖,則爲公平鎖(保證每一個線程的等待時間均衡)。須要注意的是,即便是公平鎖,JVM也沒法準確作到" 公平 ",在程序中不能以此做爲精確計算。
顯示鎖默認是非公平鎖,但能夠在構造函數中加入參數爲true來聲明出公平鎖,而synchronized實現的是非公平鎖,他不能實現公平鎖。
4.Lock是代碼級的,synchronized是JVM級的
Lock是經過編碼實現的,synchronized是在運行期由JVM釋放的,相對來講synchronized的優化可能性高,畢竟是在最核心的部分支持的,Lock的優化須要用戶自行考慮。
顯示鎖和內部鎖的功能各不相同,在性能上也稍有差異,但隨着JDK的不斷推動,相對來講,顯示鎖使用起來更加便利和強大,在實際開發中選擇哪一種類型的鎖就須要根據實際狀況考慮了:靈活、強大選擇lock,快捷、安全選擇synchronized.
線程死鎖(DeadLock)是多線程編碼中最頭疼的問題,也是最難重現的問題,由於Java是單進程的多線程語言,一旦線程死鎖,則很難經過外科手術的方法使其起死回生,不少時候只有藉助外部進程重啓應用才能解決問題,咱們看看下面的多線程代碼是否會產生死鎖:
class Foo implements Runnable { @Override public void run() { fun(10); } // 遞歸方法 public synchronized void fun(int i) { if (--i > 0) { for (int j = 0; j < i; j++) { System.out.print("*"); } System.out.println(i); fun(i); } } }
注意fun方法是一個遞歸函數,並且還加上了synchronized關鍵字,它保證同時只有一個線程可以執行,想一想synchronized關鍵字的做用:當一個帶有synchronized關鍵字的方法在執行時,其餘synchronized方法會被阻塞,由於線程持有該對象的鎖,好比有這樣的代碼:
class Foo1 { public synchronized void m1() { try { Thread.sleep(1000); } catch (InterruptedException e) { // 異常處理 } System.out.println("m1方法執行完畢"); } public synchronized void m2() { System.out.println("m2方法執行完畢"); } }
相信你們都明白,先輸出"m1執行完畢",而後再輸出"m2"執行完畢,由於m1方法在執行時,線程t持有foo對象的鎖,要想主線程得到m2方法的執行權限就必須等待m1方法執行完畢,也就是釋放當前鎖。明白了這個問題,咱們思考一下上例中帶有synchronized的遞歸方法是否能執行?會不會產生死鎖?運行結果以下:
*********9
********8
*******7
******6
*****5
****4
***3
**2
*1
一個倒三角形,沒有產生死鎖,正常執行,這是爲什麼呢?很奇怪,是嗎?那是由於在運行時當前線程(Thread-0)得到了Foo對象的鎖(synchronized雖然是標註在方法上的,但實際做用是整個對象),也就是該線程持有了foo對象的鎖,因此它能夠屢次重如fun方法,也就是遞歸了。能夠這樣來思考該問題,一個包廂有N把鑰匙,分別由N個海盜持有 (也就是咱們Java的線程了),可是同一時間只能由一把鑰匙打開寶箱,獲取寶物,只有在上一個海盜關閉了包廂(釋放鎖)後,其它海盜才能繼續打開獲取寶物,這裏還有一個規則:一旦一個海盜打開了寶箱,則該寶箱內的全部寶物對他來講都是開放的,即便是「 寶箱中的寶箱」(即內箱)對他也是開放的。能夠用以下代碼來表示:
class Foo2 implements Runnable{ @Override public void run() { method1(); } public synchronized void method1(){ method2(); } public synchronized void method2(){ //doSomething } }
方法method1是synchronized修飾的,方法method2也是synchronized修飾的,method1和method2方法重入徹底是可行的,此種狀況下會不會產生死鎖。
那什麼狀況下回產生死鎖呢?看以下代碼:
class A { public synchronized void a1(B b) { String name = Thread.currentThread().getName(); System.out.println(name + " 進入A.a1()"); try { // 休眠一秒 仍持有鎖 Thread.sleep(1000); } catch (Exception e) { // 異常處理 } System.out.println(name + " 試圖訪問B.b2()"); b.b2(); } public synchronized void a2() { System.out.println("進入a.a2()"); } } class B { public synchronized void b1(A a) { String name = Thread.currentThread().getName(); System.out.println(name + " 進入B.b1()"); try { // 休眠一秒 仍持有鎖 Thread.sleep(1000); } catch (Exception e) { // 異常處理 } System.out.println(name + " 試圖訪問A.a2()"); a.a2(); } public synchronized void b2() { System.out.println("進入B.b2()"); } }
public static void main(String[] args) throws InterruptedException { final A a = new A(); final B b = new B(); // 線程A new Thread(new Runnable() { @Override public void run() { a.a1(b); } }, "線程A").start(); // 線程B new Thread(new Runnable() { @Override public void run() { b.b1(a); } }, "線程B").start(); }
此段程序定義了兩個資源A和B,而後在兩個線程A、B中使用了該資源,因爲兩個資源之間交互操做,而且都是同步方法,所以在線程A休眠一秒鐘後,它會試圖訪問資源B的b2方法。可是B線程持有該類的鎖,並同時在等待A線程釋放其鎖資源,因此此時就出現了兩個線程在互相等待釋放資源的狀況,也就是死鎖了,運行結果以下:
線程A 進入A.a1()
線程B 進入B.b1()
線程A 試圖訪問B.b2()
線程B 試圖訪問A.a2()
此種狀況下,線程A和線程B會一直等下去,直到有外界干擾爲止,好比終止一個線程,或者某一線程自行放棄資源的爭搶,不然這兩個線程就始終處於死鎖狀態了。咱們知道達到線程死鎖須要四個條件:
只有知足了這些條件才能產生線程死鎖,這也同時告誡咱們若是要解決線程死鎖問題,就必須從這四個條件入手,通常狀況下能夠按照如下兩種方案解決:
(1)、避免或減小資源共享
一個資源被多個線程共享,若採用了同步機制,則產生死鎖的可能性大,特別是在項目比較龐大的狀況下,很難杜絕死鎖,對此最好的解決辦法就是減小資源共享。
例如一個B/S結構的辦公系統能夠徹底忽略資源共享,這是由於此類系統有三個特徵:一是併發訪問不會過高,二是讀操做多於寫操做,三是數據質量要求比較低,所以即便出現數據資源不一樣步的狀況也不可能產生太大影響,徹底能夠不使用同步技術。可是若是是一個支付清算系統就必須慎重考慮資源同步問題了,由於此係統一是數據質量要求很是高(若是產生數據不一樣步的狀況那但是重大生產事故),二是併發量大,不設置數據同步則會產生很是多的運算邏輯失效的狀況,這會致使交易失敗,產生大量的"髒數據",系統可靠性大大下降。
(2)、使用自旋鎖
回到前面的例子,線程A在等待線程B釋放資源,而線程B又在等待線程A釋放資源,僵持不下,那若是線程B設置了超時時間是否是就能夠解決該死鎖問題了呢?好比線程B在等待2秒後仍是沒法得到資源,則自行終結該任務,代碼以下:
public void b2() { try { // 馬上得到鎖,或者2秒等待鎖資源 if (lock.tryLock(2, TimeUnit.SECONDS)) { System.out.println("進入B.b2()"); } } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } }
上面的代碼中使用tryLock實現了自旋鎖(Spin Lock),它跟互斥鎖同樣,若是一個執行單元要想訪問被自旋鎖保護的共享資源,則必須先獲得鎖,在訪問完共享資源後,也必須釋放鎖。若是在獲取自旋鎖時,沒有任何執行單元保持該鎖,那麼將當即獲得鎖;若是在獲取自旋鎖時已經有保持者,那麼獲取鎖操做將"自旋" 在哪裏,直到該自旋鎖的保持者釋放了鎖爲止,在咱們的例子中就是線程A等待線程B釋放鎖,在2秒內 不斷嘗試是否可以得到鎖,達到2秒後還未得到鎖資源,線程A則結束運行,線程B將得到資源繼續執行,死鎖解除。
對於死鎖的描述最經典的案例是哲學家進餐(五位哲學家圍坐在圓形餐桌旁,人手一根筷子,作一下兩件事情:吃飯和思考。要求吃東西的時候中止思考,思考的時候中止吃東西,並且必須使用兩根筷子才能吃東西),解決此問題的方法不少,好比引入服務生(資源地調度)、資源分級等方法均可以很好的解決此類死鎖問題。在咱們Java多線程併發編程中,死鎖很難避免,也不容易預防,對付它的最好方法就是測試:提升測試覆蓋率,創建有效的邊界測試,增強資源監控,這些方法能使得死鎖無可遁形,即便發生了死鎖現象也能迅速查到緣由,提升系統性能。