JAVA線程池ThreadPoolExecutor詳解

摘要

說起進程大家都不陌生,學過操作系統的肯定都知道。而爲了不使進程由於某種不當操作而陷入阻塞,進程就出現了。在一個進程中可以創建線程,線程是比進程更小的一個粒度。我們都知道通過繼承Thread類,或者實現runnable接口來創建一個線程,然而在進程中創建和銷燬線程都會消耗大量的時間,而且大量的子線程會分享主線程的系統資源,從而會使主線程因資源受限而導致應用性能降低爲了減少線程在創建和銷燬的過程中所消耗的時間,爲了解決這些問題,線程池就由此而生。

線程池的工作原理

首先看一個例子:

假設在一臺服務器完成一項任務的時間爲T
T1 創建線程的時間
T2 在線程中執行任務的時間,包括線程間同步所需時間
T3 線程銷燬的時間
那麼T = T1+T2+T3。

分析一下:
T1,T3是多線程本身的帶來的開銷,希望減少T1,T3所用的時間,從而減少T的時間。如果在程序中頻繁的創建或銷燬線程,這導致T1和T3在T中佔有相當比例。顯然這是突出了線程的弱點(T1,T3),而不是優點(併發性)。線程池技術正是關注如何縮短或調整T1,T3時間的技術,從而提高服務器程序性能的

並且線程的創建並不僅僅是我們new一個Thread那麼簡單。當JVM把創建線程的任務交給OS時,OS創建一個線程的代價是非常昂貴的,它需要給這個線程分配內存,列入調度,在線程切換的時候還要進行內存換頁,清空cpu緩存,切換回來的時候還要重新從內存中讀取信息,破壞數據的局部性。
然後就看一下我們的線程池吧
在java.util.concurrent包中,有這樣一個類ThreadPoolExecutor,查看他的繼承關係我們可以發現
這裏寫圖片描述

  • Executor:接口 線程池必須要實現的接口 裏面只有一個方法 就是execute方法 。提交任務
  • ExecutorService:接口 繼承Executor接口 增加了submit提交任務的方式(有返回值) 以及關閉 操作
  • AbstractExecutorService實現了ExecutorService的所有方法
    ThreadPoolExecutor就是我們使用的線程池類了

而我們通常新建一個線程池會使用,Executors的靜態方法來創建幾個常用的線程池。而這幾個常用的線程池基本都是通過新建一個ThreadPoolExecutor方法實現的。
所以在介紹使用Executors線程池之前,先介紹一個ThreadPoolExecutor的構造方法

構造方法

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
變量 意義
corePoolSize 核心線程池大小
maximumPoolSize 最大線程池大小
keepAliveTime 空閒線程的存活時間
unit 空閒線程保持存活的時間的單位
workQueue 阻塞任務隊列,當要執行的任務超出corePoolSize ,那麼此時將把任務放入隊列中。
threadFactory 創建線程使用的工廠
handler 提交任務超出maxmumPoolSize+workQueue時的拒絕策略

存活時間(keepAliveTime)

keepAliveTime即空閒線程(大於corePoolSize 小於maximumPoolSize 的線程)保持存活的時間,超出這個時間,線程將被銷燬。如果設置了allowCoreThreadTimeOut(true),那麼核心線程在超出空閒時間時也會被銷燬

存活的時間單位u(unit)

空閒線程存活的時間單位,可選擇的有

TimeUnit.NANOSECONDS		//		千分之一微秒(納秒)
TimeUnit.MICROSECONDS		//		千分之一毫秒(微妙)
TimeUnit.MILLISECONDS		//		千分之一秒(毫秒)
TimeUnit.SECONDS			//		秒		
TimeUnit.MINUTES			//		分
TimeUnit.HOURS				//		小時
TimeUnit.DAYS				//		天

任務隊列(workQueue)###

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

線程工廠(threadFactory)###

每當線程需要創建一個線程時,都是通過線程工廠方法。默認的線程工廠方法將創建一個新的、非守護的線程,並且不包含特殊的配置信息。通過定製一個線程工廠,可以定製線程池的配置信息。比如當你希望爲線程池中的線程維護一些統計信息(包含有多少個線程被創建和銷燬)以及在線程被創建或者終止時調試消息寫入日誌,都可以自定義線程工廠。

拒絕策略(handler)###

當有界隊列被填滿後,拒絕策略開始發揮作用、JDK提供了集中不同的RejectedExecutionHandler 實現,每種實現都有不同的飽和策略。

  • AbortPolicy 中止策略 :默認的飽和策略,該策略將拋出爲檢查的RejectedExecutionException.調用者可以捕獲這個異常,然後根據需求編寫自己的代碼。
  • DiscardPolicy 拋棄策略: 當新提交的任務無法保存到隊列中等待執行時,拋棄策略會悄悄拋棄該任務
  • DiscardOldestPolicy 拋棄最舊的策略: 拋棄下一個將被執行的任務,然後添加新提交的任務
  • CallerRunsPolicy 調用者運行策略: 該策略實現了一種調用機制,該策略既不拋棄任務,也不拋出異常,而是將某些任務回退到調用者,從而降低新任務的流量。它不會在線程池的某個線程中執行新提交的任務,而是在一個調用了execute的線程中執行該任務(一般該調用者是main線程)

任務執行的基本流程###

這裏寫圖片描述
查看源碼後我們可以發現線程池任務的執行流程:

  • 當線程池中的線程數小於corePoolSize 時,新提交的任務直接新建一個線程執行任務(不管是否有空閒線程)
  • 當線程池中的線程數等於corePoolSize 時,新提交的任務將會進入阻塞隊列(workQueue)中,等待線程的調度
  • 當阻塞隊列滿了以後,如果corePoolSize < maximumPoolSize ,則新提交的任務會新建線程執行任務,直至線程數達到maximumPoolSize
  • 當線程數達到maximumPoolSize 時,新提交的任務會由(飽和策略)管理

在這裏簡單的測試一下CallerRunsPolicy 策略
我們新建了一個核心線程大小和最大線程大小都爲1的線程池,然後設置其拒絕策略爲CallerRunsPolicy,然後輸出任務執行的線程名稱。

/**
 *
 * @author xiaosuda
 * @date 2018/1/8
 */
public class CallerRunsPolicyTest {

    public static void main(String [] args){
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS, new LinkedBlockingDeque<>(1), new ThreadPoolExecutor.CallerRunsPolicy());
        for (int i = 0; i < 3; i++) {
            threadPoolExecutor.execute(() -> {
                try {
                    System.out.println(Thread.currentThread().getName());
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        threadPoolExecutor.shutdown();
    }
}

輸出結果:

pool-1-thread-1
main
pool-1-thread-1

分析一下:因爲核心線程池+ 阻塞隊列的大小=2,而我們新建了3個任務。當提交第一個任務時,新建線程執行任務,當提交第二個任務時,由於線程池的核心大小爲1並且有任務在執行,放入阻塞隊列,當提交第三個任務,發現阻塞隊列已經滿了,而且線程池的最大的線程個數也達到了最大。所以交給拒絕策略來處理,而CallerRunsPolicy拒絕策略會將任務回退到調用者(main線程),從而降低新任務的流量。
###向線程池提交任務###
####execute####
我們可以使用 void execute(Runnable command)方法向線程池提交任務,但是該方法沒有返回值

 ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
 cachedThreadPool.execute(new Runnable() {
             @Override
             public void run() {
                 //do some things
             }
         });

submit####

我們也可以使用 Future submit(Callable task)方法來提交任務。它會返回一個future對象,我們可以通過future.get來獲得返回值,get方法會阻塞直到線程執行成功或者拋出異常。我們也可以使用 get(long timeout, TimeUnit unit) 來阻塞一段時間後立即返回,這時候可能任務還沒有執行成功。使用場景:比如我們在使用高德地圖來查找去某個地方的路線時(路線可能有多種,但是在計算路線時耗費的時間不同),我們可以設置一個超時時間,只顯示那些已經計算完成的路線。
舉個例子,我們要得到一個100以內的隨機數。

 ExecutorService executorService = Executors.newSingleThreadExecutor();

        Future<Integer> randomNum = executorService.submit(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                return new Random().nextInt(100);
            }
        });
        try {
            System.out.println(randomNum.get());
        } catch (InterruptedException e) {
            //中斷異常
            e.printStackTrace();
        } catch (ExecutionException e) {
            // 任務異常
            e.printStackTrace();
        } finally {
            //關閉線程池
            executorService.shutdown();
        }

關閉線程池###

在ThreadPoolExecutor 中提供了關閉線程池的兩種方法。

  • shutdown
  • shutdownNow
    查看源碼後我們可以發現

shutdown:將線程池的狀態修改爲SHUTDOWN,此時無法向線程池中添加任務,否則會由拒絕策略來處理添加的任務。但是已經添加的任務(任務隊列中的任務和正在執行的任務)會繼續執行直到完成,然後線程池退出。
shutdownNow:將線程池的狀態修改爲STOP,此時無法向線程池添加任務,否則會由拒絕策略來處理添加的任務。並且任務隊列中的任務也不再執行,只執行那些正在執行的任務,並且試圖中斷它們,然後返回那些未執行的任務,線程池退出。

/**
 *
 * @author xiaosuda
 * @date 2018/1/8
 */
public class ShutdownNowTest {

    public static void main(String [] args){
        Integer threadNum = Runtime.getRuntime().availableProcessors();
        ThreadFactoryBuilder threadFactoryBuilder = new ThreadFactoryBuilder();
        threadFactoryBuilder.setNameFormat("ShutdownNowTestPool");
        ThreadFactory threadFactory = threadFactoryBuilder.build();
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, threadNum, 60L, TimeUnit.SECONDS, new LinkedBlockingDeque<>(5), threadFactory, new ThreadPoolExecutor.AbortPolicy());

        for (int i = 0; i < 10; i++) {
            String content = "Thread-name" + i;
            threadPoolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    Thread.currentThread().setName(content);
                    int x = 10000;
                    //爲了模擬長時間任務,避免任務隊列中沒有任務
                    while (x-- > 0) {

                    }
                    System.out.println( Thread.currentThread().getName());
                }
            });
        }

        List<Runnable> runnables = threadPoolExecutor.shutdownNow();
        System.out.println("--------------------------------未執行的任務--------------------------------");
        for (Runnable runnable : runnables) {
            new Thread(runnable).start();
        }
    }
}

執行結果:

Thread-name0
Thread-name6
Thread-name1
Thread-name2
Thread-name3
Thread-name7
--------------------------------未執行的任務--------------------------------
Thread-name8
Thread-name4
Thread-name5
Thread-name9

Executors框架###

在Executors,jdk爲我們提供了幾種常用的線程池

  • newFixedThreadPool:此時核心線程池數量等於線程池最大數量。 將創建一個固定長度的線程池,阻塞隊列使用的是LinkedBlockingQueue,線程可以重用。
  • newCachedThreadPool: 創建一個可以緩存的線程池,默認的核心線程池數量爲0,最大線程池數量爲Integer.MAX_VALUE.阻塞隊列使用的是SynchronousQueue。線程的存活時間爲60s.一旦有任務提交就新建一個線程執行任務,如果有空閒線程超出60s自動回收,當需求大量增加,且任務的執行時間較長時,容易oom,此時可以使用信號量來控制同時執行線程的個數。
  • newSingleThreadExecutor: 創建一個核心線程和最大線程數都爲1的線程池,阻塞隊列爲LinkedBlockingQueue。串行執行任務,此時如果在線程中爲線程池添加任務要注意避免死鎖的情況發生。
  • newScheduledThreadPool:創建一個固定長度的線程池,而且以延遲或者定時的方式執行任務。

newCachedThreadPool造成的oom####

新建了一個CachedThreadPool,添加任務時睡眠100毫秒,以到達執行長時間任務的目地。循環地向CachedThreadPool中添加任務。

/**
 *
 * @author xiaosuda
 * @date 2018/1/3
 */
public class TestExecutorsThread {

    public static void main(String [] args){
        cacheThreadPool();
    }
    private static void cacheThreadPool() {
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            cachedThreadPool.execute(() ->{
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
            });
        }
        cachedThreadPool.shutdown();
    }
}

執行結果後發現OOM:

Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
	at java.lang.Thread.start0(Native Method)
	at java.lang.Thread.start(Thread.java:717)
	at java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:950)
	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1368)
	at com.dfire.Thread.TestExecutorsThread.cacheThreadPool(TestExecutorsThread.java:19)
	at com.dfire.Thread.TestExecutorsThread.main(TestExecutorsThread.java:14)

所以我們在使用newCachedThreadPool時應該注意避免添加長時間的任務,或者限制併發執行的任務的數量。

newSingleThreadExecutor造成的死鎖####

由於newSingleThreadExecutor創建的是單一的線程,任務都在串行執行,如果任務間產生依賴,那麼就容易發生死鎖。下面舉個newSingleThreadExecutor死鎖例子。

/**
 *
 * @author xiaosuda
 * @date 2018/1/8
 */
public class SingleThreadExecutorTest {

    public static void main(String [] args) throws ExecutionException, InterruptedException {
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
        Random random = new Random();
        Future<Integer> randomSum = singleThreadExecutor.submit(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                Future<Integer> randomOne = singleThreadExecutor.submit(new Callable<Integer>() {
                    @Override
                    public Integer call() throws Exception {
                        return random.nextInt(100);
                    }
                });
                Future<Integer> randomTwo = singleThreadExecutor.submit(new Callable<Integer>() {
                    @Override
                    public Integer call() throws Exception {
                        return random.nextInt(100);
                    }
                });
                return randomOne.get() + randomTwo.get();
            }
        });
        System.out.println(randomSum.get());
    }
}

程序一直在執行,但沒有結果輸出,通過 jstack -l [pid] ->thread.jstack 命令分析
打開thread.jstack後會發現:

2018-01-08 12:38:52
Full thread dump Java HotSpot(TM) Client VM (25.131-b11 mixed mode):

"pool-1-thread-1" #11 prio=5 os_prio=0 tid=0x14e47c00 nid=0x1278 waiting on condition [0x155ff000]
   java.lang.Thread.State: WAITING (parking)
	at sun.misc.Unsafe.park(Native Method)
	- parking to wait for  <0x04750dd0> (a java.util.concurrent.FutureTask)
	at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
	at java.util.concurrent.FutureTask.awaitDone(FutureTask.java:429)
	at java.util.concurrent.FutureTask.get(FutureTask.java:191)
	at com.dfire.Thread.SingleThreadExecutorTest$1.call(SingleThreadExecutorTest.java:31)
	at com.dfire.Thread.SingleThreadExecutorTest$1.call(SingleThreadExecutorTest.java:16)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
	at java.lang.Thread.run(Thread.java:748)

   Locked ownable synchronizers:
	- <0x04742ca0> (a java.util.concurrent.ThreadPoolExecutor$Worker)

線程發生了死鎖。

閉鎖###

閉鎖時一種同步工具類,可以延遲線程的進度直到達到終止狀態,閉鎖就相當於一扇門,在閉鎖到達結束狀態之前,這扇門一直時關閉的,並且沒有任何線程能通過。當閉鎖到達結束狀態後,將不會再改變狀態,因此這扇門將永遠保持打開狀態,閉鎖可以用來確保某些活動知道其它活動都完成後才繼續執行。
CountDownLatch就是一種靈活的閉鎖實現。
比如:一個簡單的zookeeper連接。

/**
 * @author xiaosuda
 * @date 2018/01/08
 */
public class ConnectionWatcher implements Watcher {


    private static final int SESSION_TIMEOUT = 5000;
    protected ZooKeeper zooKeeper;
    private CountDownLatch connectedSignal = new CountDownLatch(1);


    public void connect(String hosts) throws IOException, InterruptedException {
        zooKeeper = new ZooKeeper(hosts, SESSION_TIMEOUT, this);
        connectedSignal.await();
    }

    @Override
    public void process(WatchedEvent watchedEvent) {
        if (watchedEvent.getState() == Event.KeeperState.SyncConnected) {
            connectedSignal.countDown();
            System.out.println("連接成功");
        }
    }

    public void close() throws InterruptedException {
        zooKeeper.close();
        System.out.println("連接關閉");
    }

    public static void main(String [] args) throws IOException, InterruptedException, KeeperException {
        ConnectionWatcher connectionWatcher = new ConnectionWatcher();
        connectionWatcher.connect("117.88.151.70:2181");
        connectionWatcher.close();
    }
}

在這裏我們使用了CountDownLatch,並且計數器的初始值爲1。在connect方法中,我們使用connectedSignal.await()進入等待狀態。ConnectionWatcher類實現了Watcher接口,別且重寫了process方法。在process方法中,如果連接成功就使計數器的值-1,將等待狀態變爲結束。開始執行連接成功的後一些操作。
還有FutureTask也可以用做閉鎖(FutureTask實現了Future語義,表示一種抽象的可生成結果的計算)。Future.get的行爲取決於任務的狀態,如果任務已經完成,那麼get會立即返回結果,否則將阻塞知道任務進入完成狀態,然後返回結果或者拋出異常。

信號量###

信號量(Semaphore)用來控制同時訪問某個特定資源的操作數量,或者同時執行某定操作的數量,信號量還可以用來實現某種資源池,或者對容器施加邊界。
信號量有兩個常用的方法:
Semaphore管理着一組虛擬的許可,許可的初始狀態可以通過構造函數來執行,在執行操作時可以首先獲得許可,並在使用以後釋放許可。如果沒有許可,那麼semaphore.acquire()將阻塞直到有許可。semaphore.release()方法將返回一個許可信號量。信號量的一種簡化形式是二值信號量,即初始值爲1的semaphore。二值信號量可以做互斥體,並不具備不可重入的加鎖語義,誰擁有這個唯一的許可,就擁有了互斥鎖。在上面的newCachedThreadPool造成的oom中,我們可以使用信號量來控制任務的併發量。修改後的代碼如下:

/**
 *
 * @author xiaosuda
 * @date 2018/1/3
 */
public class TestExecutorsThread {
    private static Integer MAX_TASK = 100;
    private static Semaphore semaphore = new Semaphore(MAX_TASK);
    public static void main(String [] args) throws InterruptedException {
        cacheThreadPool();
    }
    private static void cacheThreadPool() throws InterruptedException {
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            semaphore.acquire();
            cachedThreadPool.execute(() ->{
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                }
            });
        }
        cachedThreadPool.shutdown();
    }
}

執行後發現不會出現OOM,並且最終會全部執行成功。

柵欄###

我們已經看到通過閉鎖來啓動一組相關的操作,或者等待一組相關的操作結束。閉鎖是一次性對象,一旦進入終止狀態,就不能被重置。柵欄類似於閉鎖,它能阻塞一組線程知道某個事件發生。柵欄與閉鎖的區別在於:所有線程必須同時到達柵欄位置,才能繼續執行。到線程到達柵欄位置時調用await方法即可。閉鎖用於等待事件,而柵欄用於等待線程。柵欄用於實現一些協議,例如:明天3年紀2班的所有學生8:00在學校門口集合,到了以後要等其他人,然後一起做下一步的事情,比如去博物館。

/**
 *
 * @author xiaosuda
 * @date 2018/1/8
 */
public class CyclicBarrierTest {
    private CyclicBarrier cyclicBarrier = null;
    private ExecutorService cachedThreadPool;
    private Integer studentsNum;


    public CyclicBarrierTest(Integer studentsNum) {
        this.studentsNum = studentsNum;
        cyclicBarrier = new CyclicBarrier(studentsNum, new Runnable() {
            @Override
            public void run() {
                System.out.println("全部同學到齊,一起去博物館。");
            }
        });

    }

    public void start() {
        cachedThreadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < studentsNum; i++) {
            cachedThreadPool.execute(new Student("學生" + i));
        }
        cachedThreadPool.shutdown();
    }

    private class Student implements Runnable {
        private String name;

        public Student(String name) {
            this.name = name;
        }
        @Override
        public void run() {
            System.out.println(name + "到了");
            try {
                //等待其它學生的到來
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String [] args){

        CyclicBarrierTest cyclicBarrierTest = new CyclicBarrierTest(10);

        cyclicBarrierTest.start();

    }

}

執行結果:

學生0到了
學生1到了
學生2到了
學生3到了
學生4到了
學生5到了
學生6到了
學生7到了
學生8到了
學生9到了
全部同學到齊,一起去博物館。

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