JUC併發編程學習(十 一)-ThreadPoolExecutor線程池的學習

線程池的核心知識就是:三大方法、7個參數、拒絕策略、優化配置

線程池原理

程序運行的本質是,佔用系統資源,CPU/磁盤網絡使用。我們希望可以高效的使用資源!池化技術就是不斷的演進出來的。

  • 池化技術
    簡單的說,池化技術就是提前準備一些資源,以供使用。
    線程的創建和銷燬,以及數據庫的連接斷開都十分浪費資源。

只有是“池”,就會設計到兩個常量:

  • minSize:最小容量,核心池子的大小
  • maxSize最大容量
    這些都是爲了彈性訪問,保證系統運行的效率。

舉一個常見的例子。去銀行取錢,一般來說銀行會固定開放2個窗口供人辦理業務。還有3個業務窗口,只有等到高峯期纔會開放使用。銀行裏提供了一個等待區(候客廳)有3個位置。當你去辦理業務時,前面有人正辦理,那你就需要坐在等待區,等待傳喚。

在這裏插入圖片描述
正常情況下,core大小:2
queue大小:3
maxSize: 5
最可以存在人數:maxSize+queue =8人

爲什麼要使用線程池

  1. 提高程序執行效率
  2. 控制線程的數量,防止程序崩潰

爲了減少創建和銷燬線程的次數,讓每個線程可以多次使用,可根據系統情況調整執行的線程數量,防止消耗過多內存,所以我們可以使用線程池.

Executor 介紹

java.util.concurrent.Executor: 大部分線程池相關的接口都是實現這個接口的。

public interface Executor {

    /**
     * Executes the given command at some time in the future.  The command
     * may execute in a new thread, in a pooled thread, or in the calling
     * thread, at the discretion of the {@code Executor} implementation.
     *
     * @param command the runnable task
     * @throws RejectedExecutionException if this task cannot be
     * accepted for execution
     * @throws NullPointerException if command is null
     */
    void execute(Runnable command);
}

它的子接口和實現的類如下:
在這裏插入圖片描述

Executor接口的關係圖例(綠色實線箭頭是繼承,虛線是接口實現)
在這裏插入圖片描述

三大方法

數組有工具類Arrays,集合有工具類Collections,線程池同樣有工具類Executors。利用線程池工具類Executors就可以來創建線程池。

創建線程池的三大方法

ExecutorService threadpool1 = Executors.newFixedThreadPool(5); // 固定線程池大小
ExecutorService threadpool2 = Executors.newCachedThreadPool(); //可以彈性伸縮的線程池,遇強則強
ExecutorService threadpool3 = Executors.newSingleThreadExecutor(); // 只有一個

Executors.newFixedThreadPool(n)的使用

package com.jp.executorDemo;

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

/**
 * @className:
 * @PackageName: com.jp.executorDemo
 * @author: youjp
 * @create: 2020-05-29 16:12
 * @description:    TODO:線程池的創建使用:1.創建固定大小的線程池,超過線程池大小部分,將拒絕
 * @Version: 1.0
 */
public class ExecutorsDemo1 {
    public static void main(String[] args) {

        //1.創建線程池
        ExecutorService threadPool= Executors.newFixedThreadPool(5); //固定大小,可自行設置

        try {
            for (int i = 0; i <10 ; i++) {
                //2.使用線程池來執行線程
                threadPool.execute(()->{
                    System.out.println("線程:"+Thread.currentThread().getName()+"執行任務");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //3.關閉線程池
            threadPool.shutdown();
         }


    }
}

創建了容量大小爲5的線程池,遍歷10次去執行線程任務。會發現從始到終只有5個線程交替執行任務
在這裏插入圖片描述

Executors.newCachedThreadPool()的使用

這種線程池遇強則強,會彈性擴張,在實際的工作開發中不建議使用。

package com.jp.executorDemo;

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

/**
 * @className:
 * @PackageName: com.jp.executorDemo
 * @author: youjp
 * @create: 2020-05-29 16:12
 * @description:    TODO:線程池的創建使用:2.創建彈性的線程池,能自動擴張
 * @Version: 1.0
 */
public class ExecutorsDemo1 {
    public static void main(String[] args) {

        //1.創建線程池
        ExecutorService threadPool= Executors.newCachedThreadPool();//彈性線程池,能自動擴張

        try {
            for (int i = 0; i <10 ; i++) {
                //2.使用線程池來執行線程
                threadPool.execute(()->{
                    System.out.println("線程:"+Thread.currentThread().getName()+"執行任務");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //3.關閉線程池
            threadPool.shutdown();
        }


    }
}

有10個線程在執行任務,運行效果如下:
在這裏插入圖片描述

Executors.newSingleThreadExecutor()的使用

package com.jp.executorDemo;

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

/**
 * @className:
 * @PackageName: com.jp.executorDemo
 * @author: youjp
 * @create: 2020-05-29 16:12
 * @description:    TODO:線程池的創建使用:3.創建單線程池,只有一個線程
 * @Version: 1.0
 */
public class ExecutorsDemo1 {
    public static void main(String[] args) {

        //1.創建線程池
        ExecutorService threadPool= Executors.newSingleThreadExecutor();//單線程池

        try {
            for (int i = 0; i <10 ; i++) {
                //2.使用線程池來執行線程
                threadPool.execute(()->{
                    System.out.println("線程:"+Thread.currentThread().getName()+"執行任務");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //3.關閉線程池
            threadPool.shutdown();
        }


    }
}

只有一個線程在執行任務:
在這裏插入圖片描述
在實際的工作環境中,上述的幾種線程池創建方法都有很大的問題。禁止使用Executors去創建線程池。。我們要使用ThreadPoolExecutor 根據實際業務需要去自定義創建線程池。

阿里巴巴開發文檔有這樣寫到
在這裏插入圖片描述
使用Executors創建的線程池容易發生OOM(內存用盡). 因爲它允許的其你去隊列大小是integer最大值。

ThreadPoolExecutor 七大參數

在講 ThreadPoolExecutor 7大參數之前,我們先來分析一下Executors的三大方法創建線程池的底層代碼

//固定線程池大小
  public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
    //創建彈性線程池
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
    //創建單線程池
      public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

可以發現他們三個方法底層使用的都是去實例化一個ThreadPoolExecutor對象,設置了7個參數,這就是線程池創建的核心

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.acc = System.getSecurityManager() == null ?
            null :
            AccessController.getContext();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

7個參數

  • int corePoolSize, // 核心池子的大小
  • int maximumPoolSize, // 池子的最大大小
  • long keepAliveTime, // 空閒線程的保留時間,即在這個時間過後回收空閒的線程
  • TimeUnit unit, // 時間單位
  • BlockingQueue workQueue, // 隊列
  • ThreadFactory threadFactory, // 線程工廠,不修改!用來創建線程
  • RejectedExecutionHandler handler // 拒絕策略

既然瞭解到了線程池創建的核心,那麼我們就使用這個方法去實現之前銀行排隊辦理業務的案例。
在這裏插入圖片描述
總共8人來辦理業務,日常只開啓2個工作臺,有3個隊列位置,還有3個工作臺只有等隊列滿了,纔會開放。

package com.jp.executorDemo;

import java.util.concurrent.*;

/**
 * @className:
 * @PackageName: com.jp.executorDemo
 * @author: youjp
 * @create: 2020-05-29 16:12
 * @description 描述:線程池7大參數的使用
 * 1、只有隊列滿了,就會觸發最大線程池,否則永遠都只是corePoolSize個線程在運行,所以,隊列大小一定要根據業務情況進行設置;
 * 2、當請求線程超過線程池(maximumPoolSize + workQueue),就會觸發拒絕策略,至於怎麼拒絕,與拒絕策略RejectedExecutionHandler有關。
 */

public class ExecutorsDemo2 {
    public static void main(String[] args) {


        //1.創建線程池
        ExecutorService threadPool = new ThreadPoolExecutor(
                2, // 核心池子的大小,代表核心的2個工作臺
                5, // 線程池最大大小5,代表最大可開啓的工作臺
                2L,  // 空閒線程的保留時間
                TimeUnit.SECONDS, // 超時回收空閒的線程
                new LinkedBlockingDeque<>(3), // *根據業務設置隊列大小,隊列大小一定要設置*
                Executors.defaultThreadFactory(), // 不用變
                new ThreadPoolExecutor.AbortPolicy () //拒絕策略,這裏使用的是默認的測了:隊列滿了,就丟棄任務拋出異常!
        );

        try {
            // 隊列  RejectedExecutionException 拒絕策略
            for (int i = 1; i <= 8; i++) { //8個人
                // 默認在處理
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+" running....");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }

    }
}


因爲辦理業務是8人,核心工作臺爲2個,隊列只有3個位置,隊列已經排滿,所以會觸發最多線程池,開啓5個工作臺。
在這裏插入圖片描述
那如果共有9個人來辦理業務,而我們設置最多隻能存8人,會出現怎麼樣的結果呢?
在這裏插入圖片描述只要超過線程池大小就會觸發拒絕策略

四種拒絕策略

AbortPolicy (默認的:隊列滿了,就丟棄任務拋出異常!);
CallerRunsPolicy(哪來的回哪去? 誰叫你來的,你就去哪裏處理);
DiscardOldestPolicy (嘗試將最早進入隊列的任務刪除,嘗試加入新任務);
DiscardPolicy (隊列滿了任務也會丟棄,不拋出異常)。

AbortPolicy

使用該策略,當請求線程超過線程池(maximumPoolSize + workQueue)就會丟棄任務拋出異常

package com.jp.executorDemo;

import java.util.concurrent.*;

/**
 * @className:
 * @PackageName: com.jp.executorDemo
 * @author: youjp
 * @create: 2020-05-29 16:12
 * @description 描述:線程池7大參數的使用
 * 1、隊列滿了,就會觸發最大線程池,否則永遠都只是corePoolSize個線程在運行,所以,隊列大小一定要根據業務情況進行設置;
 * 2、當請求線程超過線程池(maximumPoolSize + workQueue),就會觸發拒絕策略,至於怎麼拒絕,與拒絕策略RejectedExecutionHandler有關。
 */

public class ExecutorsDemo2 {
    public static void main(String[] args) {


        //1.創建線程池
        ExecutorService threadPool = new ThreadPoolExecutor(
                2, // 核心池子的大小
                5, // 線程池最大大小5
                2L,  // 空閒線程的保留時間
                TimeUnit.SECONDS, // 超時回收空閒的線程
                new LinkedBlockingDeque<>(3), // *根據業務設置隊列大小,隊列大小一定要設置*
                Executors.defaultThreadFactory(), // 不用變
                new ThreadPoolExecutor.AbortPolicy () //拒絕策略
        );

        try {
            // 隊列  RejectedExecutionException 拒絕策略
            for (int i = 1; i <= 9; i++) { // 9個人
                // 默認在處理
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+" running....");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }

    }
}

我們把人數設置爲9,在執行測試
在這裏插入圖片描述

CallerRunsPolicy

使用該策略,當請求線程超過線程池(maximumPoolSize + workQueue)就會哪裏的會哪去執行。

package com.jp.executorDemo;

import java.util.concurrent.*;

/**
 * @className:
 * @PackageName: com.jp.executorDemo
 * @author: youjp
 * @create: 2020-05-29 16:12
 * @description 描述:線程池7大參數的使用
 * 1、隊列滿了,就會觸發最大線程池,否則永遠都只是corePoolSize個線程在運行,所以,隊列大小一定要根據業務情況進行設置;
 * 2、當請求線程超過線程池(maximumPoolSize + workQueue),就會觸發拒絕策略,至於怎麼拒絕,與拒絕策略RejectedExecutionHandler有關。
 */

public class ExecutorsDemo2 {
    public static void main(String[] args) {


        //1.創建線程池
        ExecutorService threadPool = new ThreadPoolExecutor(
                2, // 核心池子的大小
                5, // 線程池最大大小5
                2L,  // 空閒線程的保留時間
                TimeUnit.SECONDS, // 超時回收空閒的線程
                new LinkedBlockingDeque<>(3), // *根據業務設置隊列大小,隊列大小一定要設置*
                Executors.defaultThreadFactory(), // 不用變
                new ThreadPoolExecutor.CallerRunsPolicy () //拒絕策略
        );

        try {
            // 隊列  RejectedExecutionException 拒絕策略
            for (int i = 1; i <= 9; i++) { // 9個人
                // 默認在處理
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+" running....");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }

    }
}

執行結果
在這裏插入圖片描述

DiscardOldestPolicy

使用該策略,當請求線程超過線程池(maximumPoolSize + workQueue)就會嘗試將最早進入隊列的任務刪除,嘗試加入新任務.

package com.jp.executorDemo;

import java.util.concurrent.*;

/**
 * @className:
 * @PackageName: com.jp.executorDemo
 * @author: youjp
 * @create: 2020-05-29 16:12
 * @description 描述:線程池7大參數的使用
 * 1、隊列滿了,就會觸發最大線程池,否則永遠都只是corePoolSize個線程在運行,所以,隊列大小一定要根據業務情況進行設置;
 * 2、當請求線程超過線程池(maximumPoolSize + workQueue),就會觸發拒絕策略,至於怎麼拒絕,與拒絕策略RejectedExecutionHandler有關。
 */

public class ExecutorsDemo2 {
    public static void main(String[] args) {


        //1.創建線程池
        ExecutorService threadPool = new ThreadPoolExecutor(
                2, // 核心池子的大小
                5, // 線程池最大大小5
                2L,  // 空閒線程的保留時間
                TimeUnit.SECONDS, // 超時回收空閒的線程
                new LinkedBlockingDeque<>(3), // *根據業務設置隊列大小,隊列大小一定要設置*
                Executors.defaultThreadFactory(), // 不用變
                new ThreadPoolExecutor.DiscardOldestPolicy   () //拒絕策略
        );

        try {
            // 隊列  RejectedExecutionException 拒絕策略
            for (int i = 1; i <=15; i++) { // 15個人
                // 默認在處理
                final int temp=i;
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+" running...."+temp);
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }

    }
}


執行結果
在這裏插入圖片描述

DiscardPolicy

使用該策略,當請求線程超過線程池(maximumPoolSize + workQueue)隊列滿了,後面的任務便會丟棄,不拋出異常

package com.jp.executorDemo;

import java.util.concurrent.*;

/**
 * @className:
 * @PackageName: com.jp.executorDemo
 * @author: youjp
 * @create: 2020-05-29 16:12
 * @description 描述:線程池7大參數的使用
 * 1、隊列滿了,就會觸發最大線程池,否則永遠都只是corePoolSize個線程在運行,所以,隊列大小一定要根據業務情況進行設置;
 * 2、當請求線程超過線程池(maximumPoolSize + workQueue),就會觸發拒絕策略,至於怎麼拒絕,與拒絕策略RejectedExecutionHandler有關。
 */

public class ExecutorsDemo2 {
    public static void main(String[] args) {


        //1.創建線程池
        ExecutorService threadPool = new ThreadPoolExecutor(
                2, // 核心池子的大小
                5, // 線程池最大大小5
                2L,  // 空閒線程的保留時間
                TimeUnit.SECONDS, // 超時回收空閒的線程
                new LinkedBlockingDeque<>(3), // *根據業務設置隊列大小,隊列大小一定要設置*
                Executors.defaultThreadFactory(), // 不用變
                new ThreadPoolExecutor.DiscardPolicy   () //拒絕策略
        );

        try {
            // 隊列  RejectedExecutionException 拒絕策略
            for (int i = 1; i <=15; i++) { // 15個人
                // 默認在處理
                final int temp=i;
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+" running...."+temp);
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }

    }
}

在這裏插入圖片描述

線程池實現原理

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

1、判斷線程池裏的核心線程是否都在執行任務,如果不是(核心線程空閒或者還有核心線程沒有被創建)則創建一個新的工作線程來執行任務。如果核心線程都在執行任務,則進入下個流程。

2、線程池判斷工作隊列是否已滿,如果工作隊列沒有滿,則將新提交的任務存儲在這個工作隊列裏。如果工作隊列滿了,則進入下個流程。

3、判斷線程池裏的線程是否都處於工作狀態,如果沒有,則創建一個新的工作線程來執行任務。如果已經滿了,則交給飽和策略來處理這個任務。

優化配置

在工作中,我們應該如何合理的設置線程池的參數呢?通常我們會從2個方面去考慮

  • CPU 密集型
  • IO 密集型

CPU密集型就是根據最大能支持多少個線程同時跑,一般將線程池的maximumPoolSize(最大線程池) 參數設置與CPU處理器一樣大就可以了。可以通過如下方法獲取到服務器運行環境的CPU個數:

Runtime.getRuntime.availableProcessors();  	//獲取到所處運行環境的CPU個數

完整代碼

package com.jp.executorDemo;

import java.util.concurrent.*;

/**
 * @className:
 * @PackageName: com.jp.executorDemo
 * @author: youjp
 * @create: 2020-05-29 16:12
 * @description 描述:線程池7大參數的使用
 * 1、隊列滿了,就會觸發最大線程池,否則永遠都只是corePoolSize個線程在運行,所以,隊列大小一定要根據業務情況進行設置;
 * 2、當請求線程超過線程池(maximumPoolSize + workQueue),就會觸發拒絕策略,至於怎麼拒絕,與拒絕策略RejectedExecutionHandler有關。
 */

public class ExecutorsDemo2 {
    public static void main(String[] args) {


        //1.創建線程池
        ExecutorService threadPool = new ThreadPoolExecutor(
                2, // 核心池子的大小
                Runtime.getRuntime().availableProcessors(), // 線程池最大數
                2L,  // 空閒線程的保留時間
                TimeUnit.SECONDS, // 超時回收空閒的線程
                new LinkedBlockingDeque<>(3), // *根據業務設置隊列大小,隊列大小一定要設置*
                Executors.defaultThreadFactory(), // 不用變
                new ThreadPoolExecutor.DiscardOldestPolicy   () //拒絕策略
        );

        try {
            // 隊列  RejectedExecutionException 拒絕策略
            for (int i = 1; i <=15; i++) { // 15個人
                // 默認在處理
                final int temp=i;
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+" running...."+temp);
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }

    }
}

IO 密集型從磁盤讀寫、 一個線程在IO操作的時候、另外一個線程在CPU中跑,造成CPU空閒。最大線程數應該設置爲 IO任務數! 對於大文件的讀寫非常耗時,我們應該用單獨的線程讓他慢慢跑。

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