Java多線程之線程池 Executors 使用

Java多線程之線程池使用

前言

學習使用線程池,而不是每次用線程的時候手動去創建,然後再進行銷燬,浪費系統資源。

對此線程池應運而生,將一些線程進行復用,循環使用,等下一次業務請求時候,提前創建好的線程對其做一些處理。

線程池簡介

爲了避免系統頻繁的創建和銷燬線程,我們可以將創建的線程進行復用,提前創建好一定數量的線程,對外直接提供使用,而不是等到想用的時候手動去創建線程,使用完成之後,再去銷燬這個線程,儘可能的複用創建的這些線程。

譬如:數據庫中的數據庫連接池,tomcat連接池大小等。

線程池就是首先創建一些線程,它們的集合稱爲線程池。使用線程池可以很好地提高性能,線程池在系統啓動時即創建大量空閒的線程,程序將一個任務傳給線程池,線程池就會啓動一條線程來執行這個任務,執行結束以後,該線程並不會死亡,而是再次返回線程池中成爲空閒狀態,等待執行下一個任務。

關於線程需要知道的事

  • 使用多線程目的是爲了充分利用cpu併發多做事
  • 線程並不是越多越好
  • 線程創建,銷燬都很消耗資源
  • 多線程不一定就快

使用線程池的好處

  • 降低資源消耗。通過重複利用已創建的線程降低線程創建和銷燬造成的消耗。

  • 提高響應速度。當任務到達時,任務可以不需要等到線程創建就能立即執行。

  • 提高線程的可管理性。線程是稀缺資源,如果無限制地創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一分配、調優和監控。但是,要做到合理利用線程池,必須對其實現原理瞭如指掌。

JDK中提供的線程池

爲了讓我們更好使用和管理線程池,在JDK1.5中 提供了 Executor 框架,處於java.util.concurrent

Executor 下的接口和類繼承關係

在這裏插入圖片描述
ExecutorService 接口中的方法

在這裏插入圖片描述
常用方法說明

  • shutdownNow:執行該方法,線程池的狀態立刻變成STOP狀態,並試圖停止所有正在執行的線程,不再處理還在池隊列中等待的任務,當然,它會返回那些未執行的任務。
    它試圖終止線程的方法是通過調用Thread.interrupt()方法來實現的,但是大家知道,這種方法的作用有限,如果線程中沒有sleep 、wait、Condition、定時鎖等應用, interrupt()方法是無法中斷當前的線程的。所以,ShutdownNow()並不代表線程池就一定立即就能退出,它可能必須要等待所有正在執行的任務都執行完成了才能退出。

  • shutdown:當線程池調用該方法時,線程池的狀態則立刻變成SHUTDOWN狀態。此時,則不能再往線程池中添加任何任務,否則將會拋出RejectedExecutionException異常。但是,此時線程池不會立刻退出,直到添加到線程池中的任務都已經處理完成,纔會退出。

  • isShutDown:當調用shutdown()或shutdownNow()方法後返回爲true。

  • isTerminated:當調用shutdown()方法後,並且所有提交的任務完成後返回爲true;

  • isTerminated:當調用shutdownNow()方法後,成功停止後返回爲true;

  • submit:像線程池中提交任務,且帶返回值。

  • execute (父接口中的方法):像線程池中提交任務,無返回值。

創建線程池

創建線程時候一般使用java.util.concurrent 包中提供的工具類 Executors 創建線程池。

可以看到此類提供的一些方法

在這裏插入圖片描述

常用的有四個方法用來創建不同的線程池

public static ExecutorService newFixedThreadPool()
public static ExecutorService newSingleThreadExecutor()
public static ExecutorService newCachedThreadPool()
public static ScheduledExecutorService newSingleThreadScheduledExecutor()
public static ScheduledExecutorService newScheduledThreadPool()
  • newFixedThreadPool:該方法返回一個固定線程數量的線程池;

  • newSingleThreadExecutor:該方法返回一個只有一個現成的線程池;

  • newCachedThreadPool:返回一個可以根據實際情況調整線程數量的線程池;

  • newSingleThreadScheduledExecutor:該方法和 newSingleThreadExecutor 的區別是給定了時間執行某任務的功能,可以進行定時執行等;

  • newScheduledThreadPool:在newSingleThreadScheduledExecutor的基礎上可以指定線程數量。

經過查看源碼之後我們可以發現 上面四種創建線程方法調用的還是 ThreadPoolExecutor

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

在 Executors 內部創建線程池的時候,實際創建的都是一個 ThreadPoolExecutor 對象,只是對 ThreadPoolExecutor 構造方法,進行了默認值的設定。

ThreadPoolExecutor 的構造方法

在這裏插入圖片描述
其中這7個參數意義表示得含義是

1、corePoolSize 核心線程池大小;

當提交一個任務到線程池時,線程池會創建一個線程來執行任務,即使其他空閒的基本線程能夠執行新任務也會創建線程,等到需要執行的任務數大於線程池基本大小時就不再創建。如果調用了線程池的prestartAllCoreThreads方法,線程池會提前創建並啓動所有基本線程。

2、maximumPoolSize 線程池最大容量大小;

線程池允許創建的最大線程數。如果隊列滿了,並且已創建的線程數小於最大線程數,則線程池會再創建新的線程執行任務。值得注意的是如果使用了無界的任務隊列這個參數就沒什麼效果。

3、keepAliveTime 線程池空閒時,線程存活的時間;

線程池的工作線程空閒後,保持存活的時間。所以如果任務很多,並且每個任務執行的時間比較短,可以調大這個時間,提高線程的利用率。

4、TimeUnit 時間單位;

可選的單位有天(DAYS),小時(HOURS),分鐘(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。
在這裏插入圖片描述

5、ThreadFactory 線程工廠

用於設置創建線程的工廠,可以通過線程工廠給每個創建出來的線程設置更有意義的名字。

默認是使用自帶的線程工廠方法創建線程

在這裏插入圖片描述

一些構造方法中也可自定義創建線程工廠,一般我們使用默認就可以。

6、BlockingQueue任務隊列;

用於保存等待執行的任務的阻塞隊列。 可以選擇以下幾個阻塞隊列。

  • ArrayBlockingQueue:是一個基於數組結構的有界阻塞隊列,此隊列按 FIFO(先進先出)原則對元素進行排序。
  • LinkedBlockingQueue:一個基於鏈表結構的阻塞隊列,此隊列按FIFO (先進先出) 排序元素,吞吐量通常要高於ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool()使用了這個隊列。
  • SynchronousQueue:一個不存儲元素的阻塞隊列。每個插入操作必須等到另一個線程調用移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於LinkedBlockingQueue,靜態工廠方法Executors.newCachedThreadPool使用了這個隊列。
  • PriorityBlockingQueue:一個具有優先級的無限阻塞隊列。
7、RejectedExecutionHandler 線程拒絕策略

如果放入LinkedBlockingQueue中的任務超過整型的最大數時,拋出RejectedExecutionException。

默認自帶的拒絕則側,直接拋出異常(有界隊列情況下),也可以自定義拒絕策略。
在這裏插入圖片描述

創建線程池

首先根據 Executors 創建線程池 Executors.newScheduledThreadPool(10);

但Idea中阿里守約提示以下信息,不建議使用Executors 去創建線程。
在這裏插入圖片描述

原因是因爲:這樣子創建線程,線程池中線程最大大小默認情況下是int最大取值大小的隊列。

  • newFixedThreadPool和newSingleThreadExecutor:
      主要問題是堆積的請求處理隊列可能會耗費非常大的內存,甚至OOM。
  • newCachedThreadPool和newScheduledThreadPool:
      主要問題是線程數最大數是Integer.MAX_VALUE,可能會創建數量非常多的線程,甚至OOM。

源碼如下
在這裏插入圖片描述

所以我們儘量使用 ThreadPoolExecutor方法區創建線程池。

創建了一個核心線程是5最大線程是10 等待回收時間是60s 隊列長度是100的一個線程池

返回值得任務

  ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100));

        ArrayList<String> list = new ArrayList();

        for (int i = 0; i < 5; i++) {

            Future<String> submit = poolExecutor.submit(() -> "---> " + new Random().nextInt(3000));

            try {
                list.add(submit.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }

        }
        System.out.println("::" + poolExecutor.isTerminating());
        poolExecutor.shutdown();

        list.forEach(s -> System.out.println(s));

結果如下

—> 2321
—> 2957
—> 1381
—> 132
—> 1307

創建線程基本區別不大,唯一區別在於提交任務是否有返回值

  • execute() 方法用於提交不需要返回值的任務,所以無法判斷任務是否被線程池執行成功。通過以下代碼可知 execute() 方法輸入的任務是一個 Runnable 類的實例。

  • submit() 方法用於提交需要返回值的任務。線程池會返回一個 future 類型的對象,通過這個 future 對象可以判斷任務是否執行成功,並且可以通過 future 的 get() 方法來獲取返回值,get() 方法會阻塞當前線程直到任務完成,而使用 get(long timeout,TimeUnit unit)方法則會阻塞當前線程一段時間後立即返回,這時候有可能任務沒有執行完。

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