java多線程總結:原理結合源碼詳細講解

執行策略:線程執行的方式

串行執行

比如:醫院給病人看病的時候,可以讓所有的病人都拍成一個隊形,讓一個醫生統一的看病。醫生:線程。病人看病:任務

這種一個醫生給一羣站好隊形的病人看病--映射到java就相當於:單線程串行執行任務

映射到我們java中的話就相當於線程執行任務。

串行執行的缺點很明顯。

例如:就是假設前面有一個病人非常的慢,是一個話嘮,本來就是一個小感冒,就和醫生嘮了一天,那後面的人肯定都瘋了。

映射到我們的線程中也是一樣,如果前面的任務有很多耗時操作,後面的任務需要等待的時間很長,實時性和性能都很差。

  1. public void execute(List<Runnable> runnables) {
  2.     for (Runnable runnable : runnables) {
  3.         runnable .run();
  4.     }
  5. }

並行多線程:爲了解決上述問題,我們每個任務都創建一個線程

例如:一個醫生太慢了,現在醫院選擇針對每一個病人都安排一個醫生,這樣有話嘮也不會再影響其他病人了。但是這樣病人雖然高興了,但是醫院也瘋了,爲啥?就是醫院承受不起開支呀。

映射到我們的多線程中也是一樣的。如果針對每個任務都創建一個線程來處理的話,任務一旦多了,內存就會有很大的負載,甚至可能宕機。

  1. public void execute(List<Runnable> runnables) {
  2.     for (Runnable runnable : runnables) {
  3.         new Thread(runnable).start();
  4.     }
  5. }

線程池

例如:爲了解決上述問題,我們有應該改變策略,醫院開始改進策略,就是醫生的數量先定好,然後讓所有的病人排隊,然後每個醫生看完一個病人,就叫隊列中的下一個人。如果週六週末比較忙的時候也可以提升醫生的數量來進行看病。這樣醫院和病人開心了

映射到java中,我們創建指定數量的線程,然後把任務放在一個隊列中去處理,然後每個線程處理完任務就會去隊列中取出下一個任務執行。可以把線程重複利用,因爲創建線程也是非常消耗資源的。

所以結論就是:在一定範圍內增加線程數量的確可以提升系統的處理能力,但是過多的創建線程將會降低系統的處理速度,如果無限制的創建線程,將會使系統崩潰。

  1. public void execute(List<Runnable> runnables) {
  2. //創建隊列,存放任務
  3.     final Queue<Runnable> queue = new ConcurrentLinkedQueue<>(runnables);   
  4.     for (int index = 0; index  < 20; index ++) {
  5.         new Thread(new Runnable() {
  6.             @Override
  7.             public void run() {
  8.                 while (true) {
  9.                     Runnable r = queue.poll();
  10. //把任務處理完成就退出
  11.                     if (r == null) {    
  12.                         break;
  13.                     }
  14. //執行任務
  15.                     r.run();    
  16.                 }
  17.             }
  18.         }).start();
  19.     }
  20. }    

上述中我們創建了20個線程,讓每個線程都從任務隊列中取任務來執行,直到把任務隊列裏的任務執行完才退出。

 

多線程

在上面的3種任務的執行方式執行策略,每個執行策略都要規定很多任務的執行細節,我們要手動的去注意和關注這些細節。現在引入java中的幾個多線程的類。

  • Executor:一個接口,其定義了一個接收Runnable對象的方法executor,其方法簽名爲executor(Runnable command)
  • ExecutorService:是一個比Executor使用更廣泛的子類接口,其提供了生命週期管理的方法,以及可跟蹤一個或多個異步任務執行狀況返回Future的方法
  • AbstractExecutorService:ExecutorService執行方法的默認實現
  • ScheduledExecutorService:一個可定時調度任務的接口
  • ScheduledThreadPoolExecutor:ScheduledExecutorService的實現,一個可定時調度任務的線程池
  • ThreadPoolExecutor:線程池,可以通過調用Executors以下靜態工廠方法來創建線程池並返回一個ExecutorService對象

 

Executor接口

對於程序員來說,每次在執行某些任務的時候都要設計一種新的執行策略太麻煩了,所以java就設計了這樣的一個接口:

  1. public interface Executor {
  2.     void execute(Runnable command);
  3. }

Executor(執行器),有了這個執行器我們只需要把Runnable任務放到執行器execute方法裏就表示任務提交了,具體提交以後這些任務怎麼分配線程怎麼執行就不管了。這也就是:把任務的提交和執行解耦開來了。我們來看一下執行器怎麼用:

  1. public void execute(List<Runnable> runnables) {
  2. //創建包含10個線程的執行器
  3.     Executor executor = Executors.newFixedThreadPool(10);     
  4.     for (Runnable r : runnables) {
  5. //*提交任務
  6.         executor.execute(r);    
  7.     }
  8. }

其中的Executors類提供了一系列創建Executor子類的靜態方法(最主要有四類),newFixedThreadPool(10)方法代表創建了一個包含10個線程Executor,可以用這10個線程去執行任務。(當然如果這些不滿足業務也可以自己定義,只要繼承Executor)

  1. public class SerialExecutor implements Executor {
  2.     @Override
  3.     public void execute(Runnbale r) {
  4.         r.run();
  5.     }
  6. }

 

多線程池ThreadPoolExecutor

  1. /**
  2. *
  3. * @param corePoolSize 核心線程池大小
  4. * @param maximumPoolSize 線程池最大容量
  5. * @param keepAliveTime 線程池空閒時,線程存活的時間
  6. * @param unit 單位
  7. * @param workQueue 工作隊列
  8. * @param threadFactory 線程工廠
  9. * @param handler 處理當線程隊列滿了,也就是執行拒絕策略
  10. */
  11. public ThreadPoolExecutor(int corePoolSize,
  12. int maximumPoolSize,
  13. long keepAliveTime,
  14. TimeUnit unit,
  15. BlockingQueue<Runnable> workQueue,
  16. ThreadFactory threadFactory,
  17. RejectedExecutionHandler handler){}

 

多線程創建的工廠類Executors

Executors類裏提供了創建適用於各種場景線程池的工具方法(靜態方法),我們看一下常用的幾個:這些方法也可以去看看源碼,比較簡單

newFixedThreadPool(int nThreads)

創建一個擁有固定線程數量的線程池,具體的線程數量由nThreads參數指定。最開始該線程池中的線程數爲0,之後每提交一個任務就會創建一個線程,直到線程數等於指定的nThreads參數,此後線程數量將不再變化。

newCachedThreadPool()

創建一個可緩存的線程池。會爲每個任務都分配一個線程,但是如果一個線程執行完任務後長時間(60秒)沒有新的任務可執行,該線程將被回收

newSingleThreadExecutor()

創建單線程的線程池。其實只有一個線程,被提交到該線程的任務將在一個線程中串行執行,並且能確保任務可以按照隊列中的順序串行執行。

newScheduledThreadPool(int corePoolSize)

創建固定線程數量的線程池,而且以延遲或定時的方式來執行任務。怎麼以延遲或定時的方式執行任務呢?我們看一下該方法的返回類型ScheduledExecutorService裏提供的幾個方法:

  1. public interface ScheduledExecutorService extends ExecutorService 
  2.     public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit);
  3.     public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit);
  4.     public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit);
  5.     public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit);
  6. }

稍微解釋一下Callable和Future,Callable其實就是一個任務,只是這個任務有返回值。Future是這個線程執行中的狀態可以去獲取和控制這個線程。後面再詳解。

  1. import java.util.concurrent.Executors;
  2. import java.util.concurrent.ScheduledExecutorService;
  3. import java.util.concurrent.TimeUnit;
  4. public class XPSchedule {
  5.     private static class PrintTask implements Runnable {
  6.         private String s;
  7.         public PrintTask(String s) {
  8.             this.s = s;
  9.         }
  10.         @Override
  11.         public void run() {
  12.             System.out.println(s);
  13.         }
  14.     }
  15.     public static void main(String[] args) {
  16.         ScheduledExecutorService service = Executors.newScheduledThreadPool(10);
  17.         //隔1秒後打印
  18.         service.schedule(new PrintTask("1"), 1, TimeUnit.SECONDS);
  19.         //首次5秒後打印,每隔1秒打印一次
  20.         service.scheduleAtFixedRate(new PrintTask("2"), 51, TimeUnit.SECONDS);
  21.     }
  22. }

Callable與Future詳解:

  1. public interface Runnable {
  2.     public void run();
  3. }

它裏邊只有一個返回voidrun方法,我們定義一個計算兩個值大小的Runnable

  1. public class Task implements Runnable {
  2.     private int num;
  3.     private int num1;
  4.     public Task(int num, int num1) {
  5.         this.num = num;
  6.         this.num1 = num1;
  7.     }
  8.     @Override
  9.     public void run() {
  10.         int sum = num/num1;
  11.         System.out.println("線程t的運算結果:" + sum);
  12.     }
  13. }
  14. public class Test {
  15. public static void main(String[] args) {
  16. Task task = new Task(4, 2);
  17. Thread t = new Thread(task, "t");
  18. t.start();
  19. }
  20. }


Callable任務以及用於檢測任務執行情況的Future接口。

 

Callable是一個接口,它代表一個任務,與Runnable不同的是,這個任務是有返回值的:

  1. public interface Callable<V{
  2.     call() throws Exception;
  3. }

Task定義成一個Callable任務:

  1. import java.util.concurrent.Callable;
  2. public class Task implements Callable<Integer{
  3. private int num;
  4. private int num1;
  5. public Task(int num, int num1) {
  6. this.num = num;
  7. this.num1 = num1;
  8. }
  9. @Override
  10. public Integer call() throws Exception {
  11. int sum = num/num1;
  12. System.out.println("線程t的運算結果:" + sum);
  13. return sum;
  14. }
  15. }

call方法返回了結果。這種帶返回值的Callable任務不能像Runnable一樣直接通過Thread的構造方法傳入,在Executor的子接口ExecutorService中規定了Callable任務的提交方式:

  1. public interface ExecutorService extends Executor {
  2.     // 任務提交操作
  3.     <T> Future<T> submit(Callable<T> task);
  4.     Future<?> submit(Runnable task);
  5.     <T> Future<T> submit(Runnable task, T result);
  6.     // 生命週期管理
  7.     void shutdown();
  8.     List<Runnable> shutdownNow();
  9.     boolean isShutdown();
  10.     boolean isTerminated();
  11.     boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
  12.     // ... 省略了各種方便提交任務的方法
  13. }

 

各種線程池其實都是實現了 ExecutorService 的,Callable 任務需要提交到線程池中才能運行:

  1. public class Test {
  2.     public static void main(String[] args) {
  3.         ExecutorService service = Executors.newCachedThreadPool();
  4. //提交任務且執行,這裏需要注意一下的是submit是提交和execute功能一樣
  5.         service.submit(new Task(42));
  6.     }
  7. }

執行結果是:2

 

不管是Runnable任務還是Callable任務,線程池執行的任務可以劃分爲4個生命週期階段:

  • 創建:創建任務對象的時期。
  • 提交:調用線程池的excute或者submit方法後,將任務塞到任務隊列的時期。
  • 執行中:某個線程從任務隊列中將任務取出開始執行的時期。
  • 完成:任務執行結束。

Future接口:

  1. public interface Future<V
  2.     get() throws InterruptedException, ExecutionException;
  3.     get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
  4.     boolean isDone();
  5.     boolean cancel(boolean mayInterruptIfRunning);
  6.     boolean isCancelled();
  7. }

方法描述:

任務已經完成,那麼get方法將會立即返回,如果任務正常完成的話,會返回執行結果,若是拋出異常完成的話,將會將該異常包裝成ExecutionException後重新拋出,如果任務被取消,則調用get方法會拋出CancellationExection異常。

  1. public class Test {
  2.     public static void main(String[] args) throws Exception{
  3.         ExecutorService service = Executors.newCachedThreadPool();    //創建一個線程池
  4.         Future<Integer> future = service.submit(new Task(62)); //提交一個任務
  5.         int result = future.get();  //在任務執行完成之前,該方法將一直阻塞
  6.         System.out.println("線程main的運算結果:" + result);
  7.     }
  8. }

執行結果是:

  1. 線程t的運算結果:3
  2. 線程main的運算結果:3

有時候我們希望在指定時間內等待另一個線程的執行結果,那就可以可以使用帶時間限制的get方法,另外的幾個方法我們之後再詳細的說。

 

視線再返回到ExecutorService接口上來,除了參數類型爲Callablesubmit方法,這個接口還提供了兩個重載方法:

  1. Future<?> submit(Runnable task);    //第1個重載方法
  2. <T> Future<T> submit(Runnable task, T result);  //第2個重載方法

對於第1個只有一個Runnable參數的重載方法來說,由於Runnablerun方法並沒有返回值,也就是說任務是沒有返回值的,所以在該任務完成之後,對應的Future對象的get方法的返回值就是null。雖然不能獲得返回值,但是我們還是可以調用Future的其他方法,比如isDone表示任務是否已經完成,isCancelled表示任務是否已經被取消,cancel表示嘗試取消一個任務。

 

自定義線程

如果Executors提供的幾個創建的線程池的執行策略不能滿足你的業務,你也可以自定義線程池。只要實現了ExecutorService接口,代表着一個線程池,我們可以通過不同的構造方法參數來自定義的配置我們需要的執行策略,看一下這個類的構造方法:

  1. public XPThreadPoolExecutor(int corePoolSize,
  2.                           int maximumPoolSize,
  3.                           long keepAliveTime,
  4.                           TimeUnit unit,
  5.                           BlockingQueue<Runnable> workQueue,
  6.                           ThreadFactory threadFactory,
  7.                           RejectedExecutionHandler handler) {
  8.     // ... 省略具體實現                      
  9. }                              

相關的參數及描述如下:

剛開始的時候線程池裏並沒有線程,之後每提交一個任務就會分配一個線程,直到線程數到達corePoolSize指定的值。之後即使沒有新任務到達,這些線程也不會被銷燬。

如果線程處理任務的速度足夠快,那麼將會複用這些線程去處理任務,但是如果任務添加的速度超過了處理速度的話,線程池裏的線程數量可以繼續增加到maximumPoolSize指定的最大線程數量值時,之後便不再增加。如果線程數量已經到達最大值,但是任務的提交速度還是超過了處理速度,那麼這些任務將會被暫時放到任務隊列中,等待線程個執行完任務之後從任務隊列中取走不一定

如果某個線程在指定的keepAliveTime時間(單位是unit)內都處於空閒狀態,也就是說沒有任務可執行,那這個線程將被標記爲可回收的,但是必須當前線程池中線程數超過了corePoolSize值時,該線程將被終止。

我們可以通過這些參數來控制線程池中線程的創建與銷燬。我們之前用到的Executors.newCachedThreadPool方法創建的線程池基本大小爲0,最大大小爲最大的int值,空閒存活時間爲1分鐘;Executors.newFixedThreadPool方法創建的線程池基本大小和最大大小都是指定的參數值,空閒存活時間爲0,表示線程不會因爲長期空閒而終止。

 

管理任務隊列

線程池內部維護了一個阻塞隊列,這個隊列是用來存儲任務的,線程池的基本運行過程就是:線程調用阻塞隊列take方法,如果當前阻塞隊列中沒有任務的話,線程將一直阻塞,如果有任務提交到線程池的話,會調用該阻塞隊列put方法,並且喚醒阻塞的線程來執行任務。

線程池中的阻塞隊列的詳細用法,那我們在自定義線程池的時候該使用哪一種阻塞隊列呢?這取決於我們實際的應用場景,各種阻塞隊列其實大致可以分爲3類:

  • 無界隊列
    1. 其實無界在實際操作中的意思就是隊列容量很大很大,比如有界隊列LinkedBlockingQueue
    2. 的默認容量就是最大的int值,也就是2147483647,這個大小已經超級大了,
    3. 所以也可以被看作是無界的。
    4. 如果在線程池中使用無界隊列,而且任務的提交速度大於處理速度時,
    5. 將不斷的往隊列裏塞任務,但是內存是有限的,在隊列大到一定層度的時候,內存將被用光,
    6. 拋出OutOfMemoryError的錯誤(注意,是錯誤,不是異常)。
    7. 所以你應該對當前任務的執行速度和提交速度有所瞭解,在任務不至於積壓嚴重的情況下才使用無界隊列
    <p>&nbsp;</p>
    </li>
    <li>
    <p><span style="color:#f33b45;">有界隊列</span></p>
    
    <pre class="has" name="code"><code class="language-html hljs xml"><ol class="hljs-ln"><li><div class="hljs-ln-numbers"><div class="hljs-ln-line hljs-ln-n" data-line-number="1"></div></div><div class="hljs-ln-code"><div class="hljs-ln-line">正是因爲無界隊列可能導致內存用光,所以有界隊列看上去是一個不錯的選擇。</div></div></li><li><div class="hljs-ln-numbers"><div class="hljs-ln-line hljs-ln-n" data-line-number="2"></div></div><div class="hljs-ln-code"><div class="hljs-ln-line">但是它也有自己的問題,如果有界隊列已經被塞滿了,那後續提交的任務該怎麼辦呢?</div></div></li><li><div class="hljs-ln-numbers"><div class="hljs-ln-line hljs-ln-n" data-line-number="3"></div></div><div class="hljs-ln-code"><div class="hljs-ln-line">我們可以選擇直接把任務捨棄,或者在提交任務的線程中拋出異常,或者別的什麼處理方式,</div></div></li><li><div class="hljs-ln-numbers"><div class="hljs-ln-line hljs-ln-n" data-line-number="4"></div></div><div class="hljs-ln-code"><div class="hljs-ln-line">這種針對隊列已滿的情況下的反應措施被稱爲飽和策略,ThreadPoolExecutor構造方法中</div></div></li><li><div class="hljs-ln-numbers"><div class="hljs-ln-line hljs-ln-n" data-line-number="5"></div></div><div class="hljs-ln-code"><div class="hljs-ln-line">的handler參數就是用來幹這個,我們稍後會詳細說明各種策略採取的應對措施。</div></div></li><li><div class="hljs-ln-numbers"><div class="hljs-ln-line hljs-ln-n" data-line-number="6"></div></div><div class="hljs-ln-code"><div class="hljs-ln-line"> </div></div></li><li><div class="hljs-ln-numbers"><div class="hljs-ln-line hljs-ln-n" data-line-number="7"></div></div><div class="hljs-ln-code"><div class="hljs-ln-line">所以有界隊列 + 飽和策略的配置是我們常用的一種方案。</div></div></li></ol></code><div class="hljs-button {2}" data-title="複製" onclick="hljs.copyCode(event)"></div></pre>
    
    <p>&nbsp;</p>
    </li>
    <li>
    <p><span style="color:#f33b45;">同步移交隊列</span></p>
    
    <pre class="has" name="code"><code class="hljs vbnet"><ol class="hljs-ln"><li><div class="hljs-ln-numbers"><div class="hljs-ln-line hljs-ln-n" data-line-number="1"></div></div><div class="hljs-ln-code"><div class="hljs-ln-line">你還記得在嘮叨阻塞隊列的時候提到過一種叫SynchronousQueue的隊列麼,</div></div></li><li><div class="hljs-ln-numbers"><div class="hljs-ln-line hljs-ln-n" data-line-number="2"></div></div><div class="hljs-ln-code"><div class="hljs-ln-line">它名義上是個隊列,但底層並不維護鏈表也沒有維護數組,在一個線程調用它</div></div></li><li><div class="hljs-ln-numbers"><div class="hljs-ln-line hljs-ln-n" data-line-number="3"></div></div><div class="hljs-ln-code"><div class="hljs-ln-line">的put方法時會立即將塞入的元素轉交給調用<span class="hljs-keyword">take</span>的線程,如果沒有調用<span class="hljs-keyword">take</span></div></div></li><li><div class="hljs-ln-numbers"><div class="hljs-ln-line hljs-ln-n" data-line-number="4"></div></div><div class="hljs-ln-code"><div class="hljs-ln-line">的線程則put方法會阻塞。</div></div></li><li><div class="hljs-ln-numbers"><div class="hljs-ln-line hljs-ln-n" data-line-number="5"></div></div><div class="hljs-ln-code"><div class="hljs-ln-line"> </div></div></li><li><div class="hljs-ln-numbers"><div class="hljs-ln-line hljs-ln-n" data-line-number="6"></div></div><div class="hljs-ln-code"><div class="hljs-ln-line">使用這種阻塞隊列的線程池肯定不能堆積任務,在提交任務後必須立即被一個線程執行,</div></div></li><li><div class="hljs-ln-numbers"><div class="hljs-ln-line hljs-ln-n" data-line-number="7"></div></div><div class="hljs-ln-code"><div class="hljs-ln-line">否則的話,後續的任務提交將失敗。所以這種隊列適用於非常大或者說無界的線程池,</div></div></li><li><div class="hljs-ln-numbers"><div class="hljs-ln-line hljs-ln-n" data-line-number="8"></div></div><div class="hljs-ln-code"><div class="hljs-ln-line">因爲任務會被直接移交給執行它的線程,而不用先放到底層的數組或鏈表中,線程再從底</div></div></li><li><div class="hljs-ln-numbers"><div class="hljs-ln-line hljs-ln-n" data-line-number="9"></div></div><div class="hljs-ln-code"><div class="hljs-ln-line">層數組或鏈表中獲取,所以這種阻塞隊列性能更好。Executors.newCachedThreadPool()</div></div></li><li><div class="hljs-ln-numbers"><div class="hljs-ln-line hljs-ln-n" data-line-number="10"></div></div><div class="hljs-ln-code"><div class="hljs-ln-line">就是採用SynchronousQueue作爲底層的阻塞隊列的。</div></div></li></ol></code><div class="hljs-button {2}" data-title="複製" onclick="hljs.copyCode(event)"></div></pre>
    
    <p>&nbsp;</p>
    </li>
    

LinkedBlockingQueue或者ArrayBlockingQueue這樣的隊列都是先到達的任務會先被執行,如果你的任務有優先級的話,可以考慮使用PriorityBlockingQueue作爲阻塞隊列。

 

線程工廠

不指定這個參數,線程池就會默認創建一個非守護線程,如果不能滿足業務,也可以自己定義。

例:如果希望給線程池中的線程取個名稱、線程指定異常處理器,給線程設定優先級(最好不要,不好管理)、修改守護狀態(最好不要)。

自定義一個的ThreadFactory

  1. import java.util.concurrent.ThreadFactory;
  2. public class XPThreadFactory implements ThreadFactory {
  3.     private static int sum = 0;
  4.     private static String threadName= "xp-thread";
  5.     @Override
  6.     public Thread newThread(Runnable r) {
  7.         int i = sum++;
  8.         return new Thread(r, threadName + i);
  9.     }
  10. }

自定義了一個ThreadFactory的子類XPThreadFactory,每次調用newThread獲取的線程的名稱都會加1。

 

飽和策略

當有界隊列被任務填滿之後,應該採取的措施。在ThreadPoolExecutor裏定義了四個實現了RejectedExecutionHandler接口的靜態內部類以表示不同的應對措施:

演示一下飽和策略的用途,定義一個耗時任務:

  1. public class Task implements Runnable {
  2.     private int index;
  3.     public Task(int index) {
  4.         this.index = index;
  5.     }
  6.     @Override
  7.     public void run() {
  8.         try {
  9.             System.out.println(Thread.currentThread().getName() + 
  10. "線程正在執行第" + index + "個任務");
  11. //模擬耗時操作
  12.             Thread.sleep(9000L);     
  13.         } catch (InterruptedException e) {
  14.             throw new RuntimeException(e);
  15.         }
  16.     }
  17. }

自定義一個線程池,裏面只有一個線程並且阻塞隊列的大小爲1,來執行Task

  1. public class Test {
  2.     public static void main(String[] args) {
  3.         ExecutorService service = new ThreadPoolExecutor(
  4.                 1,  //基本大小爲1
  5.                 1,  //最大大小爲1
  6.                 0,  //表示線程不會因爲長時間空閒而被停止
  7.                 TimeUnit.SECONDS,
  8.                 new LinkedBlockingQueue<>(1),   //大小爲1的阻塞隊列
  9.                 new XPThreadFactory(),  //自定義線程工廠
  10.                 new ThreadPoolExecutor.AbortPolicy());  //飽和策略
  11.         try {
  12. //該任務會被線程立即執行
  13.             service.submit(new Task(1));  
  14.   
  15. //該任務會被塞到阻塞隊列中
  16.             service.submit(new Task(2));  
  17.   
  18. //該任務會根據不同的飽和策略而產生不同的反應
  19.             service.submit(new Task(3));    
  20.         } catch (Exception e) {
  21.             e.printStackTrace();
  22.         }
  23.     }
  24. }

結果可以看見:

第一個任務會被線程池裏唯一的線程立即執行,

第二個任務會被塞到阻塞隊列中,之後阻塞隊列就滿了,

第三個任務的時候將會根據飽和策略來產相應的應對措施措施,當前使用的是AbortPolicy,所以執行後會拋出異常:

  1. myThread0線程正在執行第1個任務
  2. java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@266474c2 rejected from java.util.concurrent.ThreadPoolExecutor@6f94fa3e[Running, pool size = 1, active threads = 1, queued tasks = 1, completed tasks = 0]
  3.     at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
  4.     at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
  5.     at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
  6.     at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:112)
  7.     at concurrency.MyThreadFactory.main(MyThreadFactory.java:30)

當把AbortPolicy飽和策略換成CallerRunsPolicy,執行結果是:

  1. myThread0線程正在執行第1個任務
  2. main線程正在執行第3個任務

CallerRunsPolicy飽和策略的意思是誰提交的任務誰執行,由於是main線程提交的任務,所以該任務由main線程去處理,由於該任務實在是太耗時了,所以main線程一直在執行該任務而無法執行後邊的代碼了~

DiscardPolicyDiscardOldestPolicy就是直接丟棄,只不過方式不同而已,自己試一試吧。

 

注意

1.當看到new Thread(r).start()這種代碼時,最好用線程池提交任務的形式來做,方便我們修改任務的執行策略。

2.線程池中的線程數量既不能太多,也不能太少。太多了的話將有大量線程在處理器和內存資源上發生競爭,太少了的話處理器資源又不能充分利用,所以在設置線程數量的時候核心原則就是:儘量使提高各種資源的利用率,而不會在線程切換上浪費過多時間,也不會因爲線程過使內存溢出。

3.在設置之前我們必須分析程序是因爲什麼受限而不能更快的運行,如果是CPU密集型的程序,我們添加過多線程並不會起到什麼效果,因爲CPU的利用率一直很高,所以一般將線程數設置成:處理器數量 + 1(這個1是爲了防止某個線程因爲某些原因而暫停,這個線程立即替換調被暫停的線程,從而最大限度的提升處理器利用率)。在java中,我們可以通過Runtime對象來獲取當前計算機的處理器數量:

int numberOfCPUs = Runtime.getRuntime().availableProcessors(); //獲取當前計算機處理器數量

對於別的密集型程序,我們通常能通過常見更多的線程來提升處理器利用率。但是線程數量也受限於依賴資源的數量,比如內存一共有有20M,每個線程需要1M的內存去運行任務,這樣我們創建多於20個線程也沒有用,因爲超過的線程會因爲分配不到內存而被迫終止。所以最優的線程數量會使得各種資源的利用率處於最高水平。

 

有依賴性任務的情況

一個任務在執行過程中依賴另一個任務的執行結果才能繼續往下執行,在線程池中可能會造成該任務永遠鎖死了

  1. import java.util.concurrent.Callable;
  2. import java.util.concurrent.ExecutorService;
  3. import java.util.concurrent.Executors;
  4. import java.util.concurrent.Future;
  5. public class DeadlockDemo {
  6.     private static ExecutorService service = Executors.newSingleThreadExecutor();
  7.     private static class Task1 implements Callable<String{
  8.         @Override
  9.         public String call() throws Exception {
  10.             System.out.println("開始執行task1");
  11.             Future<String> future = service.submit(new Task2());
  12.             System.out.println("task2的執行結果是:" + future.get());
  13.             return "task1";
  14.         }
  15.     }
  16.     private static class Task2 implements Callable<String{
  17.         @Override
  18.         public String call() throws Exception {
  19.             System.out.println("開始執行task2");
  20.             return "task2";
  21.         }
  22.     }
  23.     public static void main(String[] args) {
  24.         service.submit(new Task1());
  25.     }
  26. }
開始執行task1

當前的這個線程池中只有一個線程,這個線程執行Task1任務的時候就無法再執行其他任務。但是Task1任務的卻是將提交另一個任務,並阻塞等待該任務的執行結果,這樣程序就卡死了。

這是一個單線程線程池的極端案例,當然在線程池不夠大的時候,這樣的任務依賴導致程序僵死情況仍然可能發生,所以在有任務依賴的情況下最好不要使用線程池來執行這些任務,應該顯式的去創建線程或者分散在不同的線程池中執行任務。

任務運行處理時間差異較大,某些任務運行時間太長的情況

如果不同的任務的執行時間有長有短,它們被提交到了同一個線程池,一個線程中需要時間短的任務很快被執行完,可能該線程接着就獲取到一個時間長的任務,久而久之,線程池的所有線程都可能運行着需要時間長的任務,哪些需要時間短的任務反而都被堵在阻塞隊列中無法執行。如果出現這樣的情況,最好把需要時間長的任務和需要時間短的任務分開來處理。

任務中使用ThreadLocal的情況

ThreadLocal是對於同一個變量,每個線程看起來都好像有一個私有的值。而在線程池中的一個線程可以執行多個任務,如果在一個線程某個任務中使用了ThreadLocal變量,那當該任務執行完之後,這個線程又開始執行別的任務,上一個任務遺留下的ThreadLocal變量對這個任務是沒有意義的。除非該 ThreadLocal 變量的生命週期受限於任務的生命週期,也就是在任務執行過程中創建,在任務執行完成前銷燬。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章