【Java併發編程學習 7】深入理解線程池

什麼是線程池

Java中的線程池是運用場景最多的併發框架,幾乎所有需要異步或併發執行任務的程序都可以使用線程池。在開發過程中,合理地使用線程池能夠帶來許多好處。

  1. 降低資源消耗。通過重複利用已創建的線程降低線程創建和銷燬造成的消耗。
  2. 提高響應速度。當任務到達時,任務可以不需要等到線程創建就能立即執行。
  3. 提高線程的可管理性。線程是稀缺資源,如果無限制地創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一分配、調優和監控。但是,要做到合理利用

線程池作用

線程池是爲突然大量爆發的線程設計的,通過有限的幾個固定線程爲大量的操作服務,減少了創建和銷燬線程所需的時間,從而提高效率。

如果一個線程所需要執行的時間非常長的話,就沒必要用線程池了(不是不能作長時間操作,而是不宜。本來降低線程創建和銷燬,結果你那麼久我還不好控制還不如直接創建線程),況且我們還不能控制線程池中線程的開始、掛起、和中止。

如何實現構建線程池

什麼是ThreadPoolExecutor

ThreadPoolExecutor就是線程池

ThreadPoolExecutor其實也是JAVA的一個類,通過Executor工廠類的方法,通過傳入不同的參數,就可以構造出適用於不同應用場景下的ThreadPoolExecutor(線程池)

構造參數圖:
在這裏插入圖片描述
構造參數參數介紹:

corePoolSize 核心線程數量
maximumPoolSize 最大線程數量
keepAliveTime 線程保持時間,N個時間單位
unit 時間單位(比如秒,分)
workQueue 阻塞隊列
threadFactory 線程工廠
handler 線程池拒絕策略

什麼是Executor

Executor框架實現的就是線程池的功能。

Executors工廠類中提供的newCachedThreadPool、newFixedThreadPool 、newScheduledThreadPool 、newSingleThreadExecutor 等方法其實也只是ThreadPoolExecutor的構造函數參數不同而已。通過傳入不同的參數,就可以構造出適用於不同應用場景下的線程池,

Executor工廠類如何創建線程池圖:
在這裏插入圖片描述

線程池四種創建方式

Java通過Executors(jdk1.5併發包)提供四種線程池,分別爲:

  1. newCachedThreadPool創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閒線程,若無可回收,則新建線程。
  2. newFixedThreadPool 創建一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待。
  3. newScheduledThreadPool 創建一個定長線程池,支持定時及週期性任務執行。
  4. newSingleThreadExecutor 創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。

代碼演示構建線程池的四種方式

newCachedThreadPool

特點:newCachedThreadPool創建一個可緩存線程池,如果當前線程池的長度超過了處理的需要時,它可以靈活的回收空閒的線程,當需要增加時, 它可以靈活的添加新的線程,而不會對池的長度作任何限制

缺點:他雖然可以無線的新建線程,但是容易造成堆外內存溢出,因爲它的最大值是在初始化的時候設置爲 Integer.MAX_VALUE,一般來說機器都沒那麼大內存給它不斷使用。當然知道可能出問題的點,就可以去重寫一個方法限制一下這個最大值

總結:線程池爲無限大,當執行第二個任務時第一個任務已經完成,會複用執行第一個任務的線程,而不用每次新建線程。

代碼示例:

package com.lijie;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TestNewCachedThreadPool {
    public static void main(String[] args) {
        // 創建無限大小線程池,由jvm自動回收
        ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            final int temp = i;
            newCachedThreadPool.execute(new Runnable() {

                public void run() {
                    try {
                        Thread.sleep(100);
                    } catch (Exception e) {

                    }
                    System.out.println(Thread.currentThread().getName() + ",i==" + temp);
                }
            });
        }
    }
}

newFixedThreadPool

特點:創建一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待。定長線程池的大小最好根據系統資源進行設置。

缺點:線程數量是固定的,但是阻塞隊列是無界隊列。如果有很多請求積壓,阻塞隊列越來越長,容易導致OOM(超出內存空間)

總結:請求的擠壓一定要和分配的線程池大小匹配,定線程池的大小最好根據系統資源進行設置。如Runtime.getRuntime().availableProcessors()

Runtime.getRuntime().availableProcessors()方法是查看電腦CPU核心數量)

代碼示例:

package com.lijie;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TestNewFixedThreadPool {

    public static void main(String[] args) {
        ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 10; i++) {
            final int temp = i;
            newFixedThreadPool.execute(new Runnable() {

                public void run() {
                    System.out.println(Thread.currentThread().getName() + ",i==" + temp);
                }
            });
        }
    }
}

newScheduledThreadPool

特點:創建一個固定長度的線程池,而且支持定時的以及週期性的任務執行,類似於Timer(Timer是Java的一個定時器類)

缺點:由於所有任務都是由同一個線程來調度,因此所有任務都是串行執行的,同一時間只能有一個任務在執行,前一個任務的延遲或異常都將會影響到之後的任務(比如:一個任務出錯,以後的任務都無法繼續)。

代碼示例:

package com.lijie;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class TestNewScheduledThreadPool {
    public static void main(String[] args) {
        //定義線程池大小爲3
        ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(3);
        for (int i = 0; i < 10; i++) {
            final int temp = i;
            newScheduledThreadPool.schedule(new Runnable() {
                public void run() {
                    System.out.println("i:" + temp);
                }
            }, 3, TimeUnit.SECONDS);//這裏表示延遲3秒執行。
        }
    }
}

newSingleThreadExecutor

特點:創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,如果這個唯一的線程因爲異常結束,那麼會有一個新的線程來替代它,他必須保證前一項任務執行完畢才能執行後一項。保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。

缺點:缺點的話,很明顯,他是單線程的,高併發業務下有點無力

總結:保證所有任務按照指定順序執行的,如果這個唯一的線程因爲異常結束,那麼會有一個新的線程來替代它

代碼示例:

package com.lijie;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TestNewSingleThreadExecutor {
    public static void main(String[] args) {
        ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            final int index = i;
            newSingleThreadExecutor.execute(new Runnable() {

                public void run() {
                    System.out.println(Thread.currentThread().getName() + " index:" + index);
                    try {
                        Thread.sleep(200);
                    } catch (Exception e) {

                    }
                }
            });
        }
    }
}

自定義線程線程池

先看ThreadPoolExecutor(線程池)這個類的構造參數
在這裏插入圖片描述
構造參數參數介紹:

corePoolSize 核心線程數量
maximumPoolSize 最大線程數量
keepAliveTime 線程保持時間,N個時間單位
unit 時間單位(比如秒,分)
workQueue 阻塞隊列
threadFactory 線程工廠
handler 線程池拒絕策略

代碼示例:

package com.lijie;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class Test001 {

    public static void main(String[] args) {
        //創建線程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 2, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3));
        for (int i = 1; i <= 6; i++) {
            TaskThred t1 = new TaskThred("任務" + i);
            //executor.execute(t1);是執行線程方法
            executor.execute(t1);
        }
        //executor.shutdown()不再接受新的任務,並且等待之前提交的任務都執行完再關閉,阻塞隊列中的任務不會再執行。
        executor.shutdown();
    }
}

class TaskThred implements Runnable {
    private String taskName;

    public TaskThred(String taskName) {
        this.taskName = taskName;
    }

    public void run() {
        System.out.println(Thread.currentThread().getName() + taskName);
    }
}

線程池原理剖析

在這裏插入圖片描述
提交一個任務到線程池中,線程池的處理流程如下:

  1. 判斷線程池裏的核心線程是否都在執行任務,如果不是(核心線程空閒或者還有核心線程沒有被創建)則創建一個新的工作線程來執行任務。如果核心線程都在執行任務,則進入下個流程。
  2. 線程池判斷工作隊列是否已滿,如果工作隊列沒有滿,則將新提交的任務存儲在這個工作隊列裏。如果工作隊列滿了,則進入下個流程。
  3. 判斷線程池裏的線程是否都處於工作狀態,如果沒有,則創建一個新的工作線程來執行任務。如果已經滿了,則交給飽和策略來處理這個任務。

如何合理分配線程池大小

什麼是CPU密集

CPU密集的意思是該任務需要大量的運算,而沒有阻塞,CPU一直全速運行。

CPU密集任務只有在真正的多核CPU上纔可能得到加速(通過多線程),而在單核CPU上,無論你開幾個模擬的多線程,該任務都不可能得到加速,因爲CPU總的運算能力就那樣。

什麼是IO密集

IO密集型,即該任務需要大量的IO,即大量的阻塞。在單線程上運行IO密集型的任務會導致浪費大量的CPU運算能力浪費在等待。所以在IO密集型任務中使用多線程可以大大的加速程序運行,即時在單核CPU上,這種加速主要就是利用了被浪費掉的阻塞時間。

分配CPU和IO密集:

  1. CPU密集型時,任務可以少配置線程數,大概和機器的cpu核數相當,這樣可以使得每個線程都在執行任務

  2. IO密集型時,大部分線程都阻塞,故需要多配置線程數,2*cpu核數

合理計算

要想合理的配置線程池的大小,首先得分析任務的特性,可以從以下幾個角度分析:

  1. 任務的性質:CPU密集型任務、IO密集型任務、混合型任務。
  2. 任務的優先級:高、中、低。
  3. 任務的執行時間:長、中、短。
  4. 任務的依賴性:是否依賴其他系統資源,如數據庫連接等。

性質不同的任務可以交給不同規模的線程池執行。

  1. CPU密集型任務應配置儘可能小的線程,如配置CPU個數+1的線程數

  2. IO密集型任務應配置儘可能多的線程,因爲IO操作不佔用CPU,不要讓CPU閒下來,應加大線程數量

可以得出一個結論:
線程等待時間比CPU執行時間比例越高,需要越多線程。
線程CPU執行時間比等待時間比例越高,需要越少線程。

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