Java進階07-線程池

線程池

定義

1. 線程池是什麼?

線程池顧名思義就是事先創建若干個可執行的線程放入一個池(容器)中,需要的時候從池中獲取線程不用自行創建,使用完畢不需要銷燬線程而是放回池中,從而減少創建和銷燬線程對象的開銷。

線程池是一種多線程處理形式,處理過程中將任務添加到隊列,然後在創建線程後自動啓動這些任務。

多線程的異步執行方式,雖然能夠最大限度發揮多核計算機的計算能力,但是如果不加控制,反而會對系統造成負擔。線程本身也要佔用內存空間,大量的線程會佔用內存資源並且可能會導致Out of Memory。即便沒有這樣的情況,大量的線程回收也會給GC帶來很大的壓力。

爲了避免重複的創建線程,線程池的出現可以讓線程進行復用。通俗點講,當有工作來,就會向線程池拿一個線程,當工作完成後,並不是直接關閉線程,而是將這個線程歸還給線程池供其他任務使用。

使用

1. 線程池創建

線程池概念來源於Java中的Executor,它是一個接口,還有一個子類接口ExecutorService,一個抽象類AbstractExecutorService,真正的實現類爲ThreadPoolExecutor。ThreadPoolExecutor的構造函數提供了一系列參數來配置線程池。

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;
    }

下面解釋這些參數的含義:

1. corePoolSize:
核心池的大小,這個參數跟後面講述的線程池的實現原理有非常大的關係。在創建了線程池後,默認情況下,線程池中並沒有任何線程,而是等待有任務到來才創建線程去執行任務,除非調用了prestartAllCoreThreads()或者prestartCoreThread()方法,從這2個方法的名字就可以看出,是預創建線程的意思,即在沒有任務到來之前就創建corePoolSize個線程或者一個線程。默認情況下,在創建了線程池後,線程池中的線程數爲0,當有任務來之後,就會創建一個線程去執行任務,當線程池中的線程數目達到corePoolSize後,就會把到達的任務放到緩存隊列當中。

2. maximumPoolSize:
線程池最大線程數,這個參數也是一個非常重要的參數,它表示在線程池中最多能創建多少個線程。

3. keepAliveTime: 表示線程沒有任務執行時最多保持多久時間會終止。默認情況下,只有當線程池中的線程數大於corePoolSize時,keepAliveTime纔會起作用,直到線程池中的線程數不大於corePoolSize,即當線程池中的線程數大於corePoolSize時,如果一個線程空閒的時間達到keepAliveTime,則會終止,直到線程池中的線程數不超過corePoolSize。但是如果調用了allowCoreThreadTimeOut(boolean)方法,在線程池中的線程數不大於corePoolSize時,keepAliveTime參數也會起作用,直到線程池中的線程數爲0。

3. unit:
TimeUnit枚舉類型的值,代表keepAliveTime時間單位,可以取下列值:

    TimeUnit.DAYS; //天
  TimeUnit.HOURS; //小時
  TimeUnit.MINUTES; //分鐘
  TimeUnit.SECONDS; //秒
  TimeUnit.MILLISECONDS; //毫秒
  TimeUnit.MICROSECONDS; //微妙
  TimeUnit.NANOSECONDS; //納秒

4. workQueue: 一個阻塞隊列,用來存儲等待執行的任務,這個參數的選擇也很重要,會對線程池的運行過程產生重大影響,一般來說,這裏的阻塞隊列有以下幾種選擇

    ArrayBlockingQueue;
  LinkedBlockingQueue;
  SynchronousQueue;

ArrayBlockingQueue和PriorityBlockingQueue使用較少,一般使用LinkedBlockingQueue和Synchronous。線程池的排隊策略與BlockingQueue有關。

5. threadFactory: 線程工廠,是用來創建線程的。默認new Executors.DefaultThreadFactory();

6. handler: 線程拒絕策略。當創建的線程超出maximumPoolSize,且緩衝隊列已滿時,新任務會拒絕,有以下取值:

    ThreadPoolExecutor.AbortPolicy:丟棄任務並拋出RejectedExecutionException異常。 
  ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,但是不拋出異常。 
  ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列最前面的任務,然後重新嘗試執行任務(重複此過程)
  ThreadPoolExecutor.CallerRunsPolicy:由調用線程處理該任務

Java通過Executors工廠類提供四種線程池,分別爲:

  1. newCachedThreadPool
    可緩存線程池,先查看池中有沒有以前建立的線程,如果有,就直接使用。如果沒有,就建一個新的線程加入池中,緩存型池子通常用於執行一些生存期很短的異步型任務
// 可緩存線程池
		ExecutorService threadPool = Executors.newCachedThreadPool();
		threadPool.execute(mTestRunnable);
  1. newFixedThreadPool 創建一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待。
// 創建一個可重用固定個數的線程池
		ExecutorService threadPool = Executors.newFixedThreadPool(3);
		threadPool.execute(mTestRunnable);
  1. newScheduledThreadPool 創建一個定長線程池,支持定時及週期性任務執行。
// 定長線程池
		ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(5);
		println("定時執行");
		threadPool.schedule(mTestRunnable, 1, TimeUnit.SECONDS);
  1. newSingleThreadExecutor 創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。
    例如:
public static void main(String[] args) {
		//
		ExecutorService threadPool=Executors.newSingleThreadExecutor();
		
		threadPool.execute(mTestRunnable);
	}
	
	private static Runnable mTestRunnable=new Runnable() {
		
		@Override
		public void run() {
			println("測試線程池-----");
		}
	};

可以看到線程池的使用還是很簡單的。關鍵還是要搞清楚其實現原理。

2. 爲什麼阿里巴巴開發手冊不推薦使用Executors提供的靜態工廠方法創建線程池?
在這裏插入圖片描述

3. 線程池的關閉

ThreadPoolExecutor提供了兩個方法,用於線程池的關閉,分別是shutdown()和shutdownNow(),其中:

shutdown():不會立即終止線程池,而是要等所有任務緩存隊列中的任務都執行完後才終止,但再也不會接受新的任務

shutdownNow():立即終止線程池,並嘗試打斷正在執行的任務,並且清空任務緩存隊列,返回尚未執行的任務

原理

1. 線程池是怎麼實現複用線程的?
線程池中長期駐留了一定數量的活線程,當任務需要執行時,我們不必先去創建線程,線程池會自己選擇利用現有的活線程來處理任務。

很顯然,線程池一個很顯著的特徵就是“長期駐留了一定數量的活線程”,避免了頻繁創建線程和銷燬線程的開銷,那麼它是如何做到的呢?我們知道一個線程只要執行完了run()方法內的代碼,這個線程的使命就完成了,等待它的就是銷燬。既然這是個“活線程”,自然是不能很快就銷燬的

在分析源碼之前先來思考一下要怎麼去分析,源碼往往是比較複雜的,如果知識儲備不夠豐厚,很有可能會讀不下去,或者讀岔了。一般來講要時刻緊跟着自己的目標來看代碼,跟目標關係不大的代碼可以不理會它,一些異常的處理也可以暫不理會,先看正常的流程。就我們現在要分析的源碼而言,目標就是看看線程是如何被複用的

先重構造函數看起:

在這裏插入圖片描述

ThreadPoolExecutor這個是線程池的實現類。裏面的字段含義上面已經解釋過了。這裏再簡單回顧下

  1. corePoolSize 核心線程數
  2. maximumPoolSize 最大線程數
  3. workqueue 工作隊列-其實就是任務隊列
  4. keepAliveTime 非核心線程存活的最大時間
  5. threadFactory 線程創建工廠類
  6. handler 異常處理類

比較難理解的就是workqueue 其實它是BlockingQueue 這個是阻塞隊列。從隊列中讀取數據如果隊列爲空,那麼此線程會掛起等待 直到讀取數據。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-6Li6D1wW-1578457156011)(A832F72EB77C49329B367C4B9CB6B857)]

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

字段ctl是對線程池的運行狀態和線程池中有效線程的數量進行控制的, 它包含兩部分信息: 線程池的運行狀態 (runState) 和線程池內有效線程的數量 (workerCount)。這裏是採用的位運算。AtomicInteger是保證原子操作。大家可以運行看看結果:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-VtnnjsEr-1578457156012)(BA96EA461E5B45F889E7DBD169E3B039)]

接着看execute方法:

在這裏插入圖片描述
我們先看紅色框的判斷,如果當前工作線程數量小於核心數量,則去添加工作線程。那我們看addWorker.

在這裏插入圖片描述

首先這裏有個retry:這相當於一個goto語句。正常 break和continue就退出了,但是這個就是退到 retry處 開始。可以簡單寫一個測試一下。

首先檢測線程池運行狀態。

然後檢測線程池中的工作線程數量

這裏用了CAS保證線程同步問題。繼續往下看

在這裏插入圖片描述

創建一個工作線程Worker 並且把任務傳遞進去。

ReentrantLock鎖 同步操作

檢測運行狀態

把工作線程添加進入workers.

在這裏插入圖片描述
這個workers就是線程池中的池 存儲工作線程。數據結構是hashset。

然後調用start啓動工作線程。

接着就進入了工作線程的run方法。這個方法其實就是worker類中的run方法。

在這裏插入圖片描述
在這裏插入圖片描述

其實只要英語好的,值看這個方法的註釋就知道這個方法在幹嘛了。

這裏開啓了一個無限循環處理任務。

如果task不爲空就直接 回調 task的run方法。

如果爲空則去隊列中取出getTask();

在這裏插入圖片描述

紅框說的很清楚,執行阻塞 或者 定時等待任務 或者返回null。

如果返回null 那麼循環就退出了,線程就算是銷燬了。阻塞就會一直等待任務。

在這裏插入圖片描述

關鍵就是這個 poll和take 方法是不同的。take會一直阻塞,poll是定時。

到此爲止,其實原理已經比較清楚了。池就是hashset。裏面維護了一些 線程 他們的run方法中 開啓無線循環 從阻塞隊列中取任務,有就執行,沒有就阻塞。execute方法就根據當前的情況是 創建新的工作線程,還是往隊列中添加任務。

2. execute和submit有什麼區別?

submit方法是ExecutorService接口中定義的。execute方法是Executor接口中定義的。ExecutorService是Executor的子類

在這裏插入圖片描述

submit方法的實現在AbstractExecutorService抽象類中。

在這裏插入圖片描述

和execute 區別不大,多了結果回調。對應上面線程創建的第三種方式。

總結

  1. 所謂線程池本質是一個hashSet。多餘的任務會放在阻塞隊列中。

  2. 只有當阻塞隊列滿了後,纔會觸發非核心線程的創建。所以非核心線程只是臨時過來打雜的。直到空閒了,然後自己關閉了。

  3. 線程池提供了兩個鉤子(beforeExecute,afterExecute)給我們,我們繼承線程池,在執行任務前後做一些事情。

  4. 線程池原理關鍵技術:鎖(lock,cas)、阻塞隊列、hashSet(資源池)、位運算、同步

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