JAVA併發編程-8-線程池

上一篇看這裏:JAVA併發編程-7-併發容器

一、爲什麼要使用線程池

1、降低資源的消耗。降低線程創建和銷燬的資源消耗;
2、提高響應速度:線程的創建時間爲T1,執行時間T2,銷燬時間T3,免去T1和T3的時間
3、提高線程的可管理性。

二、手動實現一個線程池

如何實現線程呢?有2個關鍵點
1、線程必須在池子已經創建好了,並且可以保持住,要有容器保存多個線程;
2、線程還要能夠接受外部的任務,運行這個任務。容器保持這個來不及運行的任務.

/**
 * 類說明:自己線程池的實現
 */
public class MyThreadPool2 {
    // 線程池中默認線程的個數爲5
    private static int WORK_NUM = 5;
    // 隊列默認任務個數爲100
    private static int TASK_COUNT = 100;

    // 工作線程組
    private WorkThread[] workThreads;

    // 任務隊列,作爲一個緩衝
    private final BlockingQueue<Runnable> taskQueue;
    private final int worker_num;//用戶在構造這個池,希望的啓動的線程數

    // 創建具有默認線程個數的線程池
    public MyThreadPool2() {
        this(WORK_NUM, TASK_COUNT);
    }

    // 創建線程池,worker_num爲線程池中工/作線程的個數
    public MyThreadPool2(int worker_num, int taskCount) {
        if (worker_num <= 0) worker_num = WORK_NUM;
        if (taskCount <= 0) taskCount = TASK_COUNT;
        this.worker_num = worker_num;
        taskQueue = new ArrayBlockingQueue<>(taskCount);
        workThreads = new WorkThread[worker_num];
        for (int i = 0; i < worker_num; i++) {
            workThreads[i] = new WorkThread();
            workThreads[i].start();
        }
    }


    // 執行任務,其實只是把任務加入任務隊列,什麼時候執行有線程池管理器決定
    public void execute(Runnable task) {
        try {
            taskQueue.put(task);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }


    // 銷燬線程池,該方法保證在所有任務都完成的情況下才銷燬所有線程,否則等待任務完成才銷燬
    public void destroy() {
        // 工作線程停止工作,且置爲null
        System.out.println("ready close pool.....");
        for (int i = 0; i < worker_num; i++) {
            workThreads[i].stopWorker();
            workThreads[i] = null;//help gc
        }
        taskQueue.clear();// 清空任務隊列
    }

    // 覆蓋toString方法,返回線程池信息:工作線程個數和已完成任務個數
    @Override
    public String toString() {
        return "WorkThread number:" + worker_num
                + "  wait task number:" + taskQueue.size();
    }

    /**
     * 內部類,工作線程
     */
    private class WorkThread extends Thread {

        @Override
        public void run() {
            Runnable r = null;
            try {
                while (!isInterrupted()) {
                    r = taskQueue.take();
                    if (r != null) {
                        System.out.println(getId() + " ready exec :" + r);
                        r.run();
                    }
                    r = null;//help gc;
                }
            } catch (Exception e) {
                // TODO: handle exception
            }
        }

        public void stopWorker() {
            interrupt();
        }

    }
}

上面類中,在構造方法中,定義好線程容器,並且在其中創建指定數量的線程,定義了任務隊列來存放任務。

execute方法就是向任務隊列中放入需要執行的任務。

定義了WorkThread作爲單個工作線程,它不斷的從taskQueue任務隊列中去取得任務,然後去運行它。

public class TestMyThreadPool {
    public static void main(String[] args) throws InterruptedException {
        // 創建3個線程的線程池
        MyThreadPool2 t = new MyThreadPool2(3,0);
        t.execute(new MyTask("testA"));
        t.execute(new MyTask("testB"));
        t.execute(new MyTask("testC"));
        t.execute(new MyTask("testD"));
        t.execute(new MyTask("testE"));
        System.out.println(t);
        Thread.sleep(10000);
        t.destroy();// 所有線程都執行完成才destory
        System.out.println(t);
    }

    // 任務類
    static class MyTask implements Runnable {

        private String name;
        private Random r = new Random();

        public MyTask(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }

        @Override
        public void run() {// 執行任務
            try {
                Thread.sleep(r.nextInt(1000)+2000);
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getId()+" sleep InterruptedException:"
                        +Thread.currentThread().isInterrupted());
            }
            System.out.println("任務 " + name + " 完成");
        }
    }
}

我們自己實現的線程池還有很多缺點:
1,線程池在創建時,就創建啓動好了線程,如果任務數量小於線程數的時候,會導致線程無意義的等待。我們不能很好的控制線程的數量
2,阻塞隊列滿了的時候,queue只能被阻塞,線程池應用速度變慢,沒有一個有效的機制來處理這種情況

三、JDK中的線程池和工作機制

1、線程池的創建及參數

線程池的創建主要依賴ThreadPoolExecutor類,它是jdk中所有線程池的父類。它的構造參數如下:

  • int corePoolSize :線程池中核心線程數,< corePoolSize ,就會創建新線程,= corePoolSize ,這個任務就會保存到BlockingQueue,如果調用prestartAllCoreThreads()方法就會一次性的啓動corePoolSize 個數的線程。
  • int maximumPoolSize, 允許的最大線程數,BlockingQueue也滿了,< maximumPoolSize時候就會再次創建新的線程
  • long keepAliveTime, 線程空閒下來後,存活的時間,這個參數只在> corePoolSize纔有用
  • TimeUnit unit, 存活時間的單位值
  • BlockingQueue workQueue, 保存任務的阻塞隊列
  • ThreadFactory threadFactory, 創建線程的工廠,給新建的線程賦予名字
  • RejectedExecutionHandler handler :飽和策略,包括下面幾種

AbortPolicy :直接拋出異常,默認;
CallerRunsPolicy:用調用者所在的線程來執行任務
DiscardOldestPolicy:丟棄阻塞隊列裏最老的任務,隊列裏最靠前的任務
DiscardPolicy :當前任務直接丟棄
實現自己的飽和策略,實現RejectedExecutionHandler接口即可

2、提交任務的方法

execute(Runnable command) 不需要返回值
Future submit(Callable task) 需要返回值

3、關閉線程池的方法

shutdownNow():設置線程池的狀態,還會嘗試停止正在運行或者暫停任務的線程
shutdown()設置線程池的狀態,只會中斷所有沒有執行任務的線程

4、工作機制

來看一段重要的源碼:
在這裏插入圖片描述
如果小於核心線程數,就直接增加任務。否則就嘗試將任務放到等待隊列中,如果放失敗了,就在小於最大線程數的條件下進行新線程創建,如果仍然沒有成功,就拒絕任務。

在這裏插入圖片描述

四、合理配置線程池

線程池的使用也有它的合理性,並不是線程越多就越好,需要根據不同類型的任務來合理的配置線程池。

根據任務的性質來:
計算密集型(CPU),IO密集型,混合型

  • 計算密集型:這類的主要任務有 加密,大數分解,正則……., 這類任務是計算量比較大,比較耗費cpu,應該儘量減少cpu在線程之間的輪轉,所以線程數適當小一點,最大推薦:機器的Cpu核心數+1,爲什麼+1,防止頁缺失,這是指當某個線程因爲調度原因不得不去等待下一個時間片時,+1的線程可以被執行 (機器的Cpu核心=Runtime.getRuntime().availableProcessors()😉
  • IO密集型:讀取文件,數據庫連接,網絡通訊, 這類任務的特點是線程可能需要長時間等待。線程數適當大一點,可以充分的利用cpu,配置的線程數量可以參考機器的Cpu核心數*2
  • 混合型:儘量拆分,拆分成一個IO密集型和一個計算密集型,但是當IO密集型需要的時間>>計算密集型需要的時間,拆分意義不大。當IO密集型所需時間和計算密集型差不多的時候,是非常有意義的。

隊列的選擇上,應該使用有界,無界隊列可能會導致內存溢出,OOM

五、預定義的線程池

jdk中給我們預定義了一些線程池,來方便特殊情況下的使用。

  • FixedThreadPool
    創建固定線程數量的,適用於負載較重的服務器,使用了無界隊列
  • SingleThreadExecutor
    創建單個線程,需要順序保證執行任務,不會有多個線程活動,使用了無界隊列
  • CachedThreadPool
    會根據需要來創建新線程的,執行很多短期異步任務的程序,使用了SynchronousQueue
  • WorkStealingPool(JDK7以後)
    基於ForkJoinPool實現
  • ScheduledThreadPoolExecutor
    需要定期執行週期任務,Timer不建議使用了。
    newSingleThreadScheduledExecutor:只包含一個線程,只需要單個線程執行週期任務,保證順序的執行各個任務
    newScheduledThreadPool 可以包含多個線程的,線程執行週期任務,適度控制後臺線程數量的時候
    方法說明:
    schedule:只執行一次,任務還可以延時執行
    scheduleAtFixedRate:提交固定時間間隔的任務
    scheduleWithFixedDelay:提交固定延時間隔執行的任務

六、Executor框架

Executor的繼承關係大體如下:在這裏插入圖片描述
Executor框架的基本使用流程:在這裏插入圖片描述

下一篇:JAVA併發編程-9-併發安全

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