【JDK併發包基礎】線程池詳解

        爲了更好的控制多線程,JDK提供了一套線程框架Executor來幫助程序員有效的進行線程控制。Java.util.concurrent 包是專爲 Java併發編程而設計的包,它下有很多編寫好的工具:

                  

腦圖地址,感謝深入淺出 Java Concurrency ,此腦圖在這篇基礎上修改而來。其中有一個比較重要的線程工廠類:Executors。 Executors工廠會提供常用四類線程池的創建。

       以前當我們每次執行一個任務時用new Thread,頻繁創建對象會導致系統性能差,線程缺乏統一管理,可能無限制新建線程,相互之間競爭導致系統耗盡,並且缺乏定時任務,中斷等功能。線程池可以有效的提高系統資源的使用率,同時避免過多資源競爭,重用存在的線程,減少對象創建。Java通過Executors創建不同功能的線程池,若Executors無法滿足需求,我們也可以創建自定義的線程池。文章分爲以下部分講解:

       1.newFixedThreadPool()方法

       2. newSingleThreadExecutor()方法

       3.newCachedThreadPool()方法

       4.newScheduledThreadPool()方法

       5.自定義線程池

在講述之前,因爲上面5條均會用到ThreadPoolExecutor這個類,所以我們先來看看ThreadPoolExecutor中線程執行任務的示意圖,它的執行任務分兩種情況:

           

     1).Execute()方法會創建一個線程然後執行一個任務。

     2).這個線程在執行完1之後,會反覆從BlockingQueue隊列中獲取任務來執行。如果圖中所示三個線程同時間在執行任務,還有任務進來則會放入BlockingQueue隊列中暫緩起來等待線程空閒去執行。再者,這3個線程正在使用,隊列也滿了的話(有界隊列的情況),還有任務進來,則會實行拒絕策略。(take()和poll()都是取頭元素節點,區別在於前者會刪除元素,後者不會)

1.newFixedThreadPool()方法

       創建一個固定數量的線程池,裏面的線程數始終不變,當有一個線程提交時,若線程池中有空閒的線程,則立即執行。若沒有,則會暫緩在一個阻塞隊列LinkedBlockingQueue中等待有空閒的線程去執行。newFixedThreadPool()方法的源碼如下(LinkedBlockingQueue的詳解可以看博主的上一篇文章:【JDK併發包基礎】併發容器詳解):

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, //核心線程數
                                      nThreads,//最大線程數
                                      0L, //空閒時保持線程活着的時間
                                      TimeUnit.MILLISECONDS,//上述時間的單位
                                      new LinkedBlockingQueue<Runnable>());//線程池沒空閒,則新任務放在這個隊列裏
    }

       現在我們思考一下:假如有Thread1、Thread2、Thread3、Thread4四條線程分別統計C、D、E、F四個盤的大小,所有線程都統計完畢交給Thread5線程去做彙總,應當如何實現?

       第一種方式是用join()來做,不推薦:

                    

       推薦使用線程池的方式:

public static void main(String[] args) throws InterruptedException { 
  //用CountDownLatch實現,CountDownLatch傳入4相當於一個計時器,一個await需要4次countDown才能喚醒
  final CountDownLatch countDownLatch= new CountDownLatch(4);
	        Runnable run1= new Runnable() {
	            @Override
	            public void run() {
	                try {
	                    Thread.sleep(3000);
	                    System.out.println("統計C盤");
	                    countDownLatch.countDown();//單任務,把計數器減1
	                } catch (InterruptedException e) {
	                    e.printStackTrace();
	                }
	            }
	        };
	        Runnable run2= new Runnable() {
	            @Override
	            public void run() {
	                try {
	                    Thread.sleep(3000);
	                    System.out.println("統計D盤");
	                    countDownLatch.countDown();
	                } catch (InterruptedException e) {
	                    e.printStackTrace();
	                }
	            }
	        };
	        Runnable run3= new Runnable() {
	            @Override
	            public void run() {
	                try {
	                    Thread.sleep(3000);
	                    System.out.println("統計E盤");
	                    countDownLatch.countDown();
	                } catch (InterruptedException e) {
	                    e.printStackTrace();
	                }
	            }
	        };
	        Runnable run4= new Runnable() {
	            @Override
	            public void run() {
	                try {
	                    Thread.sleep(3000);
	                    System.out.println("統計F盤");
	                    countDownLatch.countDown();
	                } catch (InterruptedException e) {
	                    e.printStackTrace();
	                }
	            }
	        };
            //創建固定線程的線程池
	        ExecutorService service= Executors.newFixedThreadPool(4);
	        service.submit(run1);
	        service.submit(run2);
	        service.submit(run3);
	        service.submit(run4);
//	        new Thread(run1).start();
//	        new Thread(run2).start();
//	        new Thread(run3).start();
//	        new Thread(run4).start();
	        countDownLatch.await();//主線程,即第5線程等待
	        System.out.println("合計C,D,E,F");
	        service.shutdown();
}

       運行結果如下,統計前四個盤大小可以沒有順序,但合計始終在最後:

                                          

2. newSingleThreadExecutor()方法

       創建只有一個線程的線程池,若線程池中有空閒的線程,則立即執行。若沒有,則會暫緩在一個阻塞隊列LinkedBlockingQueue中等待有空閒的線程去執行,它保證所有任務按照提交順序執行。我們來看看newSingleThreadExecutor方法的源碼:

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService//先不用關注這個
            (new ThreadPoolExecutor(1, //核心線程數
                                    1,//最大線程數
                                    0L,//空閒時保持線程活着的時間
                                    TimeUnit.MILLISECONDS,//上面時間的單位
                                    new LinkedBlockingQueue<Runnable>()));//當線程池沒有空閒線程,就放在這個隊列裏
    }

       應用場景:這個線程池會在僅有的一個線程發生異常時,重新啓動一個線程來替代原來的線程執行下去。

3.newCachedThreadPool()方法

       創建一個可根據實際情況調整線程個數的線程池,不限制線程數量。若有任務,則創建線程。若無任務,則不創建線程,並且每一個空閒的線程會在60秒後自動回收。我們來看看源碼:

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0,//核心線程數,0表示初始化不創建線程
                                      Integer.MAX_VALUE,//int的最大值,表示不限制線程池容量
                                      60L,//緩存線程60秒
                                      TimeUnit.SECONDS,//單位
                                      new SynchronousQueue<Runnable>());
    }

       源碼中的SynchronousQueue這個沒有容量的隊列一創建,內部就使用take()方法阻塞着,當有一個線程來了直接就執行。

4.newScheduledThreadPool()方法

       創建一個大小無限的線程池,此線程池支持定時以及週期性執行任務的需求。它的源碼如下:


public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
   return new ScheduledThreadPoolExecutor(corePoolSize);
}
public class ScheduledThreadPoolExecutor  extends ThreadPoolExecutor//注意這裏繼承了ThreadPoolExecutor
        implements ScheduledExecutorService {

   public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize,//核心線程數,傳入
              Integer.MAX_VALUE,//int的最大值,表示不限制線程池容量
              0, //表示沒有延遲
              TimeUnit.NANOSECONDS,//單位是納秒
              new DelayedWorkQueue());
   }

}

       源碼中的DelayedWorkQueue是帶有延遲時間的一個隊列,其中元素只有當指定時間到了,才能夠從隊列中獲取元素,可以做定時的功能。

       創建一個任務,等3秒初始化後每隔1秒打印一句話:

public class ScheduledThread {
	public static void main(String args[]) throws Exception {
    	Temp command = new Temp();
    	//創建一個實現定時器的線程池
        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
        //command表示具體的任務對象,第一個數字表示初始化的時間,第二個數字表示輪詢的時間
        ScheduledFuture<?> scheduleTask = scheduler.scheduleWithFixedDelay(command, 3, 1, TimeUnit.SECONDS);
    }
}
class Temp extends Thread {
    public void run() {
        System.out.println("run");
    }
}

       這個類似於Java的Timer定時器,但項目中用Quartz,跟Spring整合的話,最好用@Scheduled註解。ref:Spring Schedule 任務調度實現

5.自定義線程池

        在上述Executors工廠類創建線程池時,它的創建線程方法內部實現均用了ThreadPoolExecutor這個類,ThreadPoolExecutor可以實現自定義線程池,它的構造方法如下:

 public ThreadPoolExecutor(int corePoolSize,//核心線程數
                           int maximumPoolSize,//最大線程數
                           long keepAliveTime,//線程保持多久
                           TimeUnit unit,//單位
                           BlockingQueue<Runnable> workQueue,//線程池功能
                           ThreadFactory threadFactory,//先不關注這個
                           RejectedExecutionHandler handler)//拒絕策略,比如超過最大線程數了,可以告訴客戶服務器繁忙
                             {...}

       這個構造方法對於BlockingQueue隊列是什麼類型比較關鍵,它關乎這個自定義線程池的功能。

       1.使用有界隊列ArrayBlockingQueue時,實際線程數小於corePoolSize時,則創建線程。若大於corePoolSize時,則任務會加入BlockingQueue隊列中,若隊列已滿,則在實際線程總數不大於maximumPoolSize時,創建新線程。若還大於maximumPoolSize,則執行拒絕策略,或者自定義的其他方式。

       2.使用無界隊列LinkedBlockingQueue時,緩衝隊列,當實際線程超過corePoolSize核心線程數後放置等待的線程,最後等系統空閒了在這個隊列裏取,maximumPoolSize參數在這裏就沒有作用了。因爲它是無界隊列,所以除非系統資源耗盡,否則不會出現任務入隊失敗的情況。比如創建任務的速度和處理速度差異很大,無界隊列會保持快速增長,直到系統內存耗盡。

       有界隊列和無界隊列實例如下:

public class ThreadPoolExecutorDemo implements Runnable{
	private static AtomicInteger count = new AtomicInteger(0);
	
	@Override
	public void run() {
		try {
			int temp = count.incrementAndGet();
			System.out.println("任務" + temp);
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	public static void main(String[] args) throws Exception{
		BlockingQueue<Runnable> queue = 
				new LinkedBlockingQueue<Runnable>();
				//new ArrayBlockingQueue<Runnable>(10);
		ExecutorService executor  = new ThreadPoolExecutor(
					5, 		//corePoolSize
					10, 	//使用無界隊列LinkedBlockingQueue時,maximumPoolSize這個參數值不起作用
					120L, 	//2分鐘
					TimeUnit.SECONDS,
					queue);
		
		for(int i = 0 ; i < 15; i++){//提交15個任務
			executor.execute(new ThreadPoolExecutorDemo());
		}
		Thread.sleep(1000);
		System.out.println("queue size:" + queue.size());
		executor.shutdown();
	}
}

       用LinkedBlockingQueue無界隊列執行後結果是每過一段時間5個任務一執行:

                                   

對於拒絕策略,即當任務數量超過了系統實際承載能力時該如何處理呢?JDK提供了幾種實現策略:

       AbortPolicy:直接拋出異常來阻止系統正常工作。

       CallerRunsPolicy:只要線程池未關閉,會把丟棄的任務先執行。

       DiscardOledestPolicy:丟棄最老的一個請求,嘗試再次提交當前任務

       DiscardPolicy:丟棄無法處理的任務,不給於任何處理。

這四種策略個人覺得都不太好,我們可以實現一個自定義策略,在這裏實現RejectedExecutionHandler接口就好了:

public class MyThreadPoolExecutor {
	public static void main(String[] args) {
		ThreadPoolExecutor pool = new ThreadPoolExecutor(
				1, 				//coreSize
				2, 				//MaxSize
				60, 			//60
				TimeUnit.SECONDS, 
				new ArrayBlockingQueue<Runnable>(3)			//指定一種隊列 (有界隊列)
				//new LinkedBlockingQueue<Runnable>()
				, new MyRejected()
				//, new DiscardOldestPolicy()//直接拋出異常
				);
		
		MyTask mt1 = new MyTask(1, "任務1");//第一個任務會直接執行
		MyTask mt2 = new MyTask(2, "任務2");//第二個任務會放入隊列裏,等第一個任務執行完以後才執行
		MyTask mt3 = new MyTask(3, "任務3");//因爲隊列裏有三個容量,所以任務3也會放入隊列裏
		MyTask mt4 = new MyTask(4, "任務4");//因爲隊列裏有三個容量,所以任務4也會放入隊列裏
		MyTask mt5 = new MyTask(5, "任務5");//假如有5個任務,任務1和5同時執行,任務234放在隊列裏
		MyTask mt6 = new MyTask(6, "任務6");//隊列滿了,線程池的最大線程數也超過了,則會實行拒絕策略
		
		pool.execute(mt1);
		pool.execute(mt2);
		pool.execute(mt3);
		pool.execute(mt4);
		pool.execute(mt5);
		pool.execute(mt6);
		
		pool.shutdown();
	}
}
class MyRejected implements RejectedExecutionHandler{
	@Override
	//傳入當前任務對象和當前線程池對象
	public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
		//1.可以做一些處理,比如用http再創建請求給傳數據的客戶端,讓它重新發送任務。高峯期的時候,系統已經超負荷了,不建議再發送請求
		//2.只是記錄日誌:id及相關重要的信息,暫緩到磁盤上,在不是高峯期的時候跑一些定時的job解析日誌,把沒處理的任務再處理一遍或者批處理下,一般用這個
		System.out.println("自定義處理..");
		System.out.println("當前被拒絕任務爲:" + r.toString());
	}
}
class MyTask implements Runnable {
	private int taskId;
	private String taskName;
	
	public MyTask(int taskId, String taskName){this.taskId = taskId;this.taskName = taskName;}
	public int getTaskId() {return taskId;}
	public void setTaskId(int taskId) {this.taskId = taskId;}
	public String getTaskName() {return taskName;}
	public void setTaskName(String taskName) {this.taskName = taskName;}
	
	@Override
	public void run() {
		try {
			System.out.println("run taskId =" + this.taskId);
			Thread.sleep(3000);
			//System.out.println("end taskId =" + this.taskId);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}		
	}
	public String toString(){
		return Integer.toString(this.taskId);
	}
}

        運行結果如下:

                                             

        到這裏已經介紹完了Java併發包下的線程池,博主是個普通的程序猿,水平有限,文章難免有錯誤,歡迎犧牲自己寶貴時間的讀者,就本文內容直抒己見。

系列:

【JDK併發包基礎】線程池詳解

【JDK併發包基礎】併發容器詳解

【JDK併發包基礎】工具類詳解

【JDK併發基礎】Java內存模型詳解

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