線程池參數原理及應用

線程池原理

    Java創建一個線程很方便,只需new Thread()就可以, 但是當有多個任務需要進行進行處理時,頻繁的進行創建和啓用線程同樣需要系統開銷,也不利於管理,於是同mysql的連接池一樣,自然有對線程的管理池即線程池。

    做個比喻,線程池好比一個公司,那麼線程本身就是一個個的員工,來對線程的創建和銷燬進行管理,最大化的進行資源的合理調度。

    Java的線程池創建也很簡單,concurrent這個併發包下有Executors可以很方便的進行四種常用線程的創建:

    newFixedThreadPool:創建固定數量的線程的線程池,可以控制最大併發數,常用於知道具體任務的數量,需要進行多線程的操作,如批量插入數據庫任務,需要進行10萬條數據分頁,每1萬條數據一頁,配置一個線程處理,一共配置10個線程,進行並行批量插入,就可以使用這個線程池來進行,大大減少響應時間

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

    newCachedThreadPool: 創建可一段時間內重複利用的線程池,常用於不知道具體的任務數量,但是還需要進行並行處理的情況,如springboot @Aysnc就可以指定使用這個線程池,來進行一些埋點等的各種業務的異步處理

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

    newSingleThreadExecutor: 創建單個線程的線程池,這個線程池可以在線程死後(或發生異常時)重新啓動一個線程來替代原來的線程繼續執行下去!

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

    newScheduledThreadPool: 創建一個可以定時和重複執行的線程池,常用於定時任務和延時任務的執行線程池

public ScheduledThreadPoolExecutor(int corePoolSize,
                                       ThreadFactory threadFactory) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue(), threadFactory);
    }

    當然線程池還可以自定義,Java只是提供了幾種常用的靜態線程池的創建方法,以上也已經將4種線程池的創建源碼顯示出來了,可以發現線程池的創建都是通過new ThreadPoolExecutor()來實現的,現在主要介紹下幾個重要的參數和接口:

    首先ThreadPoolExecutor繼承了AbstractExecutorService類,並提供了四個構造器,AbstractExecutorService又實現了ExecutorService接口,ExecutorService接口繼承了只有一個方法execute的Executor。

   下面解釋下一下構造器中各個參數的含義:

  • corePoolSize:核心池的大小,這個參數跟後面講述的線程池的實現原理有非常大的關係。在創建了線程池後,默認情況下,線程池中並沒有任何線程,而是等待有任務到來才創建線程去執行任務,除非調用了prestartAllCoreThreads()或者prestartCoreThread()方法,從這2個方法的名字就可以看出,是預創建線程的意思,即在沒有任務到來之前就創建corePoolSize個線程或者一個線程。默認情況下,在創建了線程池後,線程池中的線程數爲0,當有任務來之後,就會創建一個線程去執行任務,當線程池中的線程數目達到corePoolSize後,就會把到達的任務放到緩存隊列當中;
  • maximumPoolSize:線程池最大線程數,這個參數也是一個非常重要的參數,它表示在線程池中最多能創建多少個線程;
  • keepAliveTime:表示線程沒有任務執行時最多保持多久時間會終止。默認情況下,只有當線程池中的線程數大於corePoolSize時,keepAliveTime纔會起作用,直到線程池中的線程數不大於corePoolSize,即當線程池中的線程數大於corePoolSize時,如果一個線程空閒的時間達到keepAliveTime,則會終止,直到線程池中的線程數不超過corePoolSize。但是如果調用了allowCoreThreadTimeOut(boolean)方法,在線程池中的線程數不大於corePoolSize時,keepAliveTime參數也會起作用,直到線程池中的線程數爲0;
  • unit:參數keepAliveTime的時間單位,有7種取值,分別代表一種時間的單位,秒,分,小時等:
  • workQueue:一個阻塞隊列,用來存儲等待執行的任務,這個參數的選擇也很重要,會對線程池的運行過程產生重大影響,一般來說,這裏的阻塞隊列有以下幾種選擇:  
ArrayBlockingQueue; 有界阻塞隊列,由數組實現,需要指定數組大小
LinkedBlockingQueue; 無界阻塞隊列,由鏈表實現,最大值是Integer的最大值 
SynchronousQueue; 這個隊列不會保存提交的任務,而是將直接新建一個線程來執行新來的任務。
  • threadFactory:線程工廠,主要用來創建線程;
  • handler:表示當拒絕處理任務時的策略,有以下四種取值:
ThreadPoolExecutor.AbortPolicy:丟棄任務並拋出RejectedExecutionException異常。 
ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,但是不拋出異常。 
ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列最前面的任務,然後重新嘗試執行任務(重複此過程)
ThreadPoolExecutor.CallerRunsPolicy:由調用線程處理該任務

    其中注意這幾個參數都是volatile修飾的,用來保證多線程下的可見性,我們也可以根據這些參數的不同配置,來產生我們需要的線程池。

    有了線程池後,我們需要關注幾個線程池的狀態:

        

    下圖表明幾個狀態之間的轉化關係:

        

    接下來就是舉個栗子來表明如何使用:

    ExecutorService executorService = Executors.newFixedThreadPool(15);

    在執行完上述代碼後,我們其實就創建了一個有15個核心線程數量,最大也是15個線程數量,空閒線程保存時間爲1分鐘,採用無限阻塞隊列,任務拒絕採用AbortPolicy:丟棄任務並拋出RejectedExecutionException異常的線程池。在創建後,並沒有進行活躍的線程工人產生,可用線程數爲0,比如接下來有10個任務進來,就會創建10個線程工人來進行工作,並且工作完不會銷燬,之後又來了10個任務,之前的10個線程還沒有處理完他們自己的任務,這個時候就又會創建5個線程工人來進行任務的處理,有小夥伴有疑問了,那剩下的5個任務怎麼辦呢,對了,還有阻塞隊列,這些沒有工人處理的任務會進入待辦事項般的阻塞隊列,先進先出,待15個工人將手頭的活辦完之後進行依次處理,因爲阻塞隊列是無界阻塞隊列,因此,任務會不斷的丟到這個隊列中,所以,並不會創建因爲隊列太小,而不得已創建幾個個臨時工來處理,這個幾個數量即在最大線程和核心線程之間的差值數量,這些臨時線程的有效時間只有keepAliveTime的時間,此外在來了多個任務之後,如果隊列是有界的,且任務數超過了最大能夠創建的線程數,即工人不能再招了,待辦事項列表也滿了,這個時候公司舊不幹了,拋出異常,任務拒絕策略。

    接下了是實戰,結合CompletableFuture進行展示:

    簡單介紹下CompletableFuture:CompletableFuture提供了非常強大的Future的擴展功能,可以幫助我們簡化異步編程的複雜性,並且提供了函數式編程的能力,可以通過回調的方式處理計算結果,也提供了轉換和組合 CompletableFuture 的方法,結合線程池可以達到併發編程的目的    

package cn.chinotan;

import lombok.extern.slf4j.Slf4j;
import org.junit.Test;

import java.util.List;
import java.util.concurrent.*;

/**
 * @program: test
 * @description: 多線程測試
 * @author: xingcheng
 * @create: 2019-03-23 17:27
 **/
@Slf4j
public class ExecutorTest {

    @Test
    public void test() {
        ExecutorService executorService = Executors.newFixedThreadPool(15);

        CompletableFuture[] completableFutures = new CompletableFuture[15];
        List<Integer> integers = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 15; i++) {
            int finalI = i;
            CompletableFuture<Integer> integerCompletableFuture = CompletableFuture.supplyAsync(() -> costMethod(finalI), executorService)
                    .whenComplete((r, e) -> {
                        if (null != e) {
                           e.printStackTrace(); 
                        } else {
                            integers.add(r);
                        }
                    });

            completableFutures[i] = integerCompletableFuture;
        }

        CompletableFuture.allOf(completableFutures).join();
        long count = integers.stream().count();
        log.info("一共處理成功:{}", count);
    }

    /**
     * 耗時的操作
     *
     * @param i
     * @return
     */
    public int costMethod(int i) {
        try {
            TimeUnit.SECONDS.sleep(5);
            log.info("耗時的操作 {}", i);
            return 1;
        } catch (InterruptedException e) {
            e.printStackTrace();
            return 0;
        }
    }
}

    運行結果:

    可以看到15個耗時的操作很快就並行執行完成,並且還能返回執行的成功結果數

    以上就是我對線程池的理解和應用,歡迎大家關注和瀏覽提問,謝謝大家

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