產品經理問我:手動建立線程不香嗎,爲何非要用線程池呢?

據說微信搜索《Java魚仔》會變動強哦!java

本文收錄於JavaStarter ,裏面有我完整的Java系列文章,學習或面試均可以看看哦git

每次寫線程池的文章時,總會想起本身大三第一次面試就是掛在這上面,當時年少輕狂,連SpringBoot是什麼都不知道就敢面阿里,真是初生牛犢不怕虎。程序員

(一)什麼是線程池

線程(thread)是操做系統可以進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運做單位,咱們的程序最終都是由線程進行運做。在Java中,建立和銷燬線程的動做是很消耗資源的,所以就出現了所謂「池化資源」技術。github

線程池是池化資源技術的一個應用,所謂線程池,顧名思義就是預先按某個規定建立若干個可執行線程放入一個容器中(線程池),須要使用的時候從線程池中去取,用完以後不銷燬而是放回去,從而減小了線程建立和銷燬的次數,達到節約資源的目的。web

(二)爲何要使用線程池

2.1 下降資源消耗

前面已經講到線程池的出現減小了線程建立和銷燬的次數,每一個線程均可以被重複利用,可執行多個任務。面試

2.2 提升系統的響應速度

每當有任務到來時,直接複用線程池中的線程,而不須要等待新線程的建立,這個動做能夠帶來響應速度的提高spring

2.3 防止過多的線程搞壞系統

能夠根據系統的承受能力,調整線程池中的工做線程的數量,防止由於線程過多服務器變慢或死機。java一個線程默認佔用空間爲1M,能夠想象一旦手動建立線程過多極有可能致使內存溢出。緩存

(三)線程池主要參數

咱們能夠用Executors類來建立一些經常使用的線程池,可是像阿里是禁止直接經過Executors類直接建立線程池的,具體的緣由稍後再談。服務器

在瞭解Executors類所提供的幾個線程池前,咱們首先來了解一下 ThreadPoolExecutor的主要參數,ThreadPoolExecutor是建立線程池的類,咱們選取參數最多的構造方法來看一下:微信

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
名稱 類型 含義
corePoolSize int 核心線程池的大小
maximumPoolSize int 最大線程池大小
keepAliveTime long 線程最大空閒時間
unit TimeUnit 時間單位
workQueue BlockingQueue<Runnable> 線程等待隊列
threadFactory ThreadFactory 線程建立工程
handler RejectedExecutionHandler 拒絕策略

3.1 corePoolSize

當向線程池提交一個任務時,若是線程池中已建立的線程數量小於corePoolSIze,即使存在空閒線程,也會建立一個新線程來執行任務,直到建立的線程數大於或等於corePoolSIze。

3.2 maximumPoolSize

線程池所容許的最大線程個數,當隊列滿了且已經建立的線程數小於maximumPoolSize時,會建立新的線程執行任務。

3.3 keepAliveTime

當線程中的線程數大於corePoolSIze時,若是線程空閒時間大於keepAliveTime,該線程就會被銷燬。

3.4 unit

keepAliveTime的時間單位

3.5 workQueue

用於保存等待執行任務的隊列

3.6 threadFactory

用於建立新線程。threadFactory建立的線程也是採用new Thread()方式,threadFactory建立的線程名都具備統一的風格:pool-m-thread-n

3.7 handler

拒絕策略,當線程池和隊列滿了以後,再加入新線程後會執行此策略。 下面是四種線程池的拒絕策略:

AbortPolicy:中斷任務並拋出異常

DiscardPolicy:中段任務可是不拋出異常

DiscardOldestPolicy:丟棄隊列中最老的任務,而後嘗試提交新任務

CallerRunsPolicy:由調用線程處理該任務

(四)線程池執行流程

當咱們瞭解了ThreadPoolExecutor的七個參數後,咱們就能夠很快的理解線程池的流程:

在這裏插入圖片描述

當提交任務後,首先判斷當前線程數是否超過核心線程數,若是沒超過則建立新線程執行任務,不然判斷工做隊列是否已滿,若是未滿則將任務添加到隊列中,不然判斷線程數是否超過最大線程數,若是未超過則建立線程執行任務,不然執行拒絕策略。

(五)Executors提供的線程池

executors提供了許多種線程池供用戶使用,雖然不少公司禁止使用executors建立線程池,可是對於剛開始解除線程池的人來講,Executors類所提供的線程池能很好的帶你進入多線程的世界。

5.1 newSingleThreadExecutor

ExecutorService executorService = Executors.newSingleThreadExecutor();

聽名字就能夠知道這是一個單線程的線程池,在這個線程池中只有一個線程在工做,至關於單線程執行全部任務,此線程能夠保證全部任務的執行順序按照提交順序執行,看構造方法也能夠看出,corePoolSize和maximumPoolSize都是1。

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

5.2 newFixedThreadPool

ExecutorService executorService = Executors.newFixedThreadPool(2);

固定長度的線程池,線程池的長度在建立時經過變量傳入。下面是newFixedThreadPool的構造方法,corePoolSize和maximumPoolSize都是傳入的參數值

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

5.3 newCachedThreadPool

ExecutorService executorService = Executors.newCachedThreadPool();

可緩存線程池,這個線程池設定keepAliveTime爲60秒,而且對最大線程數量幾乎不作控制。

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

觀察構造方法,corePoolSize = 0,maximumPoolSize = Integer.MAX_VALUE,即線程數量幾乎無限制。設定keepAliveTime 爲60秒,線程空閒60秒後自動結束,由於該線程池建立無限制,不會有隊列等待,因此使用SynchronousQueue同步隊列。

5.4 newScheduledThreadPool

建立一個定時的線程池。此線程池支持定時以及週期性執行任務的需求。下面是newScheduledThreadPool的用法:

Thread thread1=new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"thread1");
    }
});
Thread thread2=new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"thread2");
    }
});
Thread thread3=new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"thread3");
    }
});
ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
//在1000ms後執行thread1
scheduledExecutorService.schedule(thread1,1000,TimeUnit.MILLISECONDS);
//在1000ms後每隔1000ms執行一次thread2,若是任務執行時間比間隔時間長,則延遲執行
scheduledExecutorService.scheduleAtFixedRate(thread2,1000,1000,TimeUnit.MILLISECONDS);
//和第二種方式相似,但下一次任務開始的時間爲:上一次任務結束時間(而不是開始時間) + delay時間
scheduledExecutorService.scheduleWithFixedDelay(thread3,1000,1000,TimeUnit.MILLISECONDS);

(六)爲何阿里巴巴禁止程序員用Exectors建立線程池

若是你的idea裝了Alibaba Java Codeing Guidelines插件(推薦你們使用,有助於讓你的代碼更加規範),那麼當你寫了Exectors建立線程池後會看到提示:

在這裏插入圖片描述

而且阿里將這個用法定義爲Blocker,即不容許使用,而是讓人們用ThreadPoolExecutor的方式建立線程池。緣由是經過ThreadPoolExecutor的方式,這樣的處理方式讓寫的人更加明確線程池的運行規則,規避資源耗盡的風險。

在這裏插入圖片描述

Executors返回的線程池對象的弊端以下:

1)FixedThreadPool和SingleThreadPool:

  容許的請求隊列長度爲Integer.MAX_VALUE,可能會堆積大量的請求,從而致使OOM。    2)CachedThreadPool:

  容許的建立線程數量爲Integer.MAX_VALUE,可能會建立大量的線程,從而致使OOM。    下面是ThreadPoolExecutor建立線程池的簡單例子

int corePoolSize=5;
int maximumPoolSize=10;
long keepAliveTime=30;
BlockingQueue<Runnable> blockingQueue=new ArrayBlockingQueue(2);
RejectedExecutionHandler handler=new ThreadPoolExecutor.AbortPolicy();
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.MILLISECONDS, blockingQueue, handler);
threadPoolExecutor.execute(thread1);

(七)在SpringBoot項目中使用線程池

SpringBoot對線程池又作了一層封裝,在SpringBoot中,能夠經過ThreadPoolTaskExecutor類來建立線程池。須要注意二者的區別ThreadPoolExecutor時JUC包下的類,ThreadPoolTaskExecutor是springframework包下的類。但原理都是同樣的。

7.1 項目搭建

首先搭建一個SpringBoot項目,只須要引入web依賴便可

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

7.2 配置文件配置

線程池的參數儘可能寫到配置文件裏,這樣就能根據須要進行修改,在application.properties文件中配置:

myExecutor.corePoolSize=5
myExecutor.maxPoolSize=10
myExecutor.keepAliveSeconds=30
myExecutor.allowCoreThreadTimeOut=false
myExecutor.queueCapacity=20
myExecutor.threadNamePrefix=myExecutor-

7.3 編寫一個本身的線程池

新建一個包叫config,新建類ExecutorConfig ,首先參數的值從配置文件中獲取,接着用ThreadPoolTaskExecutor 建立一個本身的線程池,注入到Bean容器中。

@Configuration
@EnableAsync
public class ExecutorConfig {

    @Value("${myExecutor.corePoolSize}")
    private int corePoolSize;
    @Value("${myExecutor.maxPoolSize}")
    private int maxPoolSize;
    @Value("${myExecutor.keepAliveSeconds}")
    private int keepAliveSeconds;
    @Value("${myExecutor.allowCoreThreadTimeOut}")
    private boolean allowCoreThreadTimeOut;
    @Value("${myExecutor.queueCapacity}")
    private int queueCapacity;
    @Value("${myExecutor.threadNamePrefix}")
    private String threadNamePrefix;

    @Bean("myExecutor")
    public Executor myExecutor(){
        ThreadPoolTaskExecutor executor=new ThreadPoolTaskExecutor();
        //核心線程數
        executor.setCorePoolSize(corePoolSize);
        //最大線程數
        executor.setMaxPoolSize(maxPoolSize);
        //線程空閒時間
        executor.setKeepAliveSeconds(keepAliveSeconds);
        //是否保留核心線程數
        executor.setAllowCoreThreadTimeOut(allowCoreThreadTimeOut);
        //隊列長度
        executor.setQueueCapacity(queueCapacity);
        //拒絕策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        //設置線程名稱前綴
        executor.setThreadNamePrefix(threadNamePrefix);
        executor.initialize();
        return executor;
    }
}

這裏須要注意的是有一個方法setAllowCoreThreadTimeOut,當傳入參數爲true時,全部線程超時後都會被銷燬,若是爲false,只有超過核心線程數而且超時時纔會被銷燬。

7.4 編寫service並使用

首先寫一個service接口:

public interface DoSomeThing {
    /**
     * 經過線程池異步執行耗時的任務
     */
    public void doSomeThing();
}

再編寫實現類:

@Service
public class DoSomeThingImpl implements DoSomeThing {

    @Override
    @Async("myExecutor")
    public void doSomeThing() {
        System.out.println(Thread.currentThread().getName()+"-in");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"-out");
    }
}

增長註解@Async("myExecutor")後,這段方法就會異步由myExecutor線程池執行。

7.5 編寫一個controller

新建一個IndexController,每次請求執行一次doSomeThing()方法。

@RestController
public class IndexController {

    @Autowired
    private DoSomeThing doSomeThing;

    @RequestMapping(value = "/index",method = RequestMethod.GET)
    public String index(){
        doSomeThing.doSomeThing();
        return "success";
    }
}

7.6 測試

訪問十次http://localhost:8080/index,因爲設置的核心線程數是5,隊列容量是30,所以最多隻會用到5個線程資源,結果以下:

在這裏插入圖片描述

(八)總結

線程池不算什麼很難的技術,可是必定要掌握,面試也會常常問,工做中也會用到。要趁別人不注意的時候悄悄拔尖,而後驚豔全部人,咱們下期再見。

相關文章
相關標籤/搜索