Java 執行緒池詳細介紹

ImportNew發表於2015-10-20

根據摩爾定律(Moore’s law),積體電路電晶體的數量差不多每兩年就會翻一倍。但是電晶體數量指數級的增長不一定會導致 CPU 效能的指數級增長。處理器製造商花了很多年來提高時脈頻率和指令並行。在新一代的處理器上,單執行緒程式的執行速率確實有所提高。但是,時脈頻率不可能無限制地提高,如處理器 AMD FX-9590 的時脈頻率達到5 GHz,這已經非常困難了。如今處理器製造商更喜歡採用多核處理器(multi-core processors)。擁有4核的智慧手機已經非常普遍,更不用提手提電腦和桌上型電腦。結果,軟體不得不採用多執行緒的方式,以便能夠更好的使用硬體。執行緒池可以幫助程式設計師更好地利用多核 CPU。

執行緒池

好的軟體設計不建議手動建立和銷燬執行緒。執行緒的建立和銷燬是非常耗 CPU 和記憶體的,因為這需要 JVM 和作業系統的參與。64位 JVM 預設執行緒棧是大小1 MB。這就是為什麼說在請求頻繁時為每個小的請求建立執行緒是一種資源的浪費。執行緒池可以根據建立時選擇的策略自動處理執行緒的生命週期。重點在於:在資源(如記憶體、CPU)充足的情況下,執行緒池沒有明顯的優勢,否則沒有執行緒池將導致伺服器奔潰。有很多的理由可以解釋為什麼沒有更多的資源。例如,在拒絕服務(denial-of-service)攻擊時會引起的許多執行緒並行執行,從而導致執行緒飢餓(thread starvation)。除此之外,手動執行執行緒時,可能會因為異常導致執行緒死亡,程式設計師必須記得處理這種異常情況。

即使在你的應用中沒有顯式地使用執行緒池,但是像 Tomcat、Undertow這樣的web伺服器,都大量使用了執行緒池。所以瞭解執行緒池是如何工作的,怎樣調整,對系統效能優化非常有幫助。

執行緒池可以很容易地通過 Executors 工廠方法來建立。JDK 中實現 ExecutorService 的類有:

  • ForkJoinPool
  • ThreadPoolExecutor
  • ScheduledThreadPoolExecutor

這些類都實現了執行緒池的抽象。下面的一小段程式碼展示了 ExecutorService 的生命週期:

public List<Future<T>> executeTasks(Collection<Callable<T>> tasks) {
    // create an ExecutorService
    // 建立 ExecutorService
    final ExecutorService executorService = Executors.newSingleThreadExecutor();

    // execute all tasks
    // 執行所有任務
    final List<Future<T>> executedTasks = executorService.invokeAll(tasks);

    // shutdown the ExecutorService after all tasks have completed
    // 所有任務執行完後關閉 ExecutorService
    executorService.shutdown();

    return executedTasks;
}

首先,建立一個最簡單的 ExecutorService —— 一個單執行緒的執行器(executor)。它用一個執行緒來處理所有的任務。當然,你也可以通過各種方式自定義 ExecutorService,或者使用 Executors 類的工程方法來建立 ExecutorService:

newCachedThreadPool() :建立一個 ExecutorService,該 ExecutorService 根據需要來建立執行緒,可以重複利用已存在的執行緒來執行任務。

newFixedThreadPool(int numberOfThreads) :建立一個可重複使用的、固定執行緒數量的 ExecutorService。

newScheduledThreadPool(int corePoolSize):根據時間計劃,延遲給定時間後建立 ExecutorService(或者週期性地建立 ExecutorService)。

newSingleThreadExecutor():建立單個工作執行緒 ExecutorService。

newSingleThreadScheduledExecutor():根據時間計劃延遲建立單個工作執行緒 ExecutorService(或者週期性的建立)。

newWorkStealingPool():建立一個擁有多個任務佇列(以便減少連線數)的 ExecutorService。

在上面這個例子裡,所有的任務都只執行一次,你也可以使用其他方法來執行任務:

  • void execute(Runnable)
  • Future submit(Callable)
  • Future submit(Runnable)

最後,關閉 executorService。Shutdown() 是一個非阻塞式方法。呼叫該方法後,ExecutorService 進入“關閉模式(shutdown mode)”,在該模式下,之前提交的任務都會執行完成,但是不會接收新的任務。如果想要等待任務執行完成,需要呼叫 awaitTermination() 方法。

ExecutorService 是一個非常有用的工具,可以幫助我們很方便執行所有的任務。它的好處在什麼地方呢?我們不需要手動建立工作執行緒。一個工作執行緒就是 ExecutorService 內部使用的執行緒。值得注意的是,ExecutorService 管理執行緒的生命週期。它可以在負載增加的時候增加工作執行緒。另一方面,在一定週期內,它也可以減少空閒的執行緒。當我們使用執行緒池的時候,我們就不再需要考慮執行緒本身。我們只需要考慮非同步處理的任務。此外,當出現不可預期的異常時,我們不再需要重複建立執行緒,我們也不需要擔心當一個執行緒執行完任務後的重複使用問題。最後,一個任務提交以後,我們可以獲取一個未來結果的抽象——Future。當然,在 Java 8中,我們可以使用更優秀的 CompletableFuture,如何將一個 Future 轉換為 CompletableFuture 已超出了本文所討論的範圍。但是請記住,只有提交的任務是一個 Callable 時,Future 才有意義,因為 Callable 有輸出結果,而 Runnable 沒有。

內部組成

每個執行緒池由幾個模組組成:

  • 一個任務佇列,
  • 一個工作執行緒的集合,
  • 一個執行緒工廠,
  • 管理執行緒狀態的後設資料。

ExecutorService 介面有很多實現,我們重點關注一下最常用的 ThreadPoolExecutor。實際上,newCachedThreadPool()、newFixedThreadPool() 和 newSingleThreadExecutor() 三個方法返回的都是 ThreadPoolExecutor 類的例項。如果要手動建立一個ThreadPoolExecutor 類的例項,至少需要5個引數:

  • int corePoolSize:執行緒池儲存的執行緒數量。
  • int maximumPoolSize:執行緒的最大數量。
  • long keepAlive and TimeUnit unit:超出 corePoolSize 大小後,執行緒空閒的時間到達給定時間後將會關閉。
  • BlockingQueue workQueue:提交的任務將被放置在該佇列中等待執行。

Java執行緒池介紹

阻塞佇列

LinkedBlockingQueue 是呼叫 Executors 類中的方法生成 ThreadPoolExecutor 例項時使用的預設佇列,PriorityBlockingQueue 實際上也是一個BlockingQueue,不過,根據設定的優先順序來處理任務也是一個棘手的問題。首先,提交一個 Runnable 或 Callable 任務,該任務被包裝成一個 RunnableFuture,然後新增到佇列中,ProrityBlockingQueue 比較每個物件來決定執行的優先權(比較物件是包裝後的RunnableFuture而不是任務的內容)。不僅如此,當 corePoolSize 大於1並且工作執行緒空閒時,ThreadPoolExecutor 可能會根據插入順序來執行,而不是 PriorityBlockingQueue 所期望的優先順序順序。

預設情況下,ThreadPoolExecutor 的工作佇列(workQueue)是沒有邊界的。通常這是沒問題的,但是請記住,沒有邊界的工作佇列可能導致應用出現記憶體溢位(out of memory)錯誤。如果要限制任務佇列的大小,可以設定 RejectionExecutionHandler。你可以自定義處理器或者從4個已有處理器(預設AbortPolicy)中選擇一個:

  • CallerRunsPolicy
  • AbortPolicy
  • DiscardPolicy
  • DiscardOldestPolicy

執行緒工廠

執行緒工廠通常用於建立自定義的執行緒。例如,你可以增加自定義的 Thread.UncaughtExceptionHandler 或者設定執行緒名稱。在下面的例子中,使用執行緒名稱和執行緒的序號來記錄未捕獲的異常。

public class LoggingThreadFactory implements ThreadFactory {

    private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
    private static final String THREAD_NAME_PREFIX = "worker-thread-";

    private final AtomicInteger threadCreationCounter = new AtomicInteger();

    @Override
    public Thread newThread(Runnable task) {
        int threadNumber = threadCreationCounter.incrementAndGet();
        Thread workerThread = new Thread(task, THREAD_NAME_PREFIX + threadNumber);

        workerThread.setUncaughtExceptionHandler(thread, throwable -> logger.error("Thread {} {}", thread.getName(), throwable));

        return workerThread;
    }
}

生產者消費者例項

生產者消費者是一種常見的同步多執行緒處理問題。在這個例子中,我們使用 ExecutorService 解決此問題。但是,這不是解決該問題的教科書例子。我們的目標是演示執行緒池來處理所有的同步問題,從而程式設計師可以集中精力去實現業務邏輯。

Producer 定期的從資料庫獲取新的資料來建立任務,並將任務提交給 ExecutorService。ExecutorService 管理的執行緒池中的一個工作執行緒代表一個 Consumer,用於處理業務任務(如計算價格並返回給客戶)。

首先,我們使用 Spring 來配置:

@Configuration
public class ProducerConsumerConfiguration {

    <a href='http://www.jobbole.com/members/weibo_1902876561'>@Bean</a>
    public ExecutorService executorService() {
        // single consumer
        return Executors.newSingleThreadExecutor();
    }

    // other beans such as a data source, a scheduler, etc.
}

然後,建立一個 Consumer 及一個 ConsumerFactory。該工程方法通過生產者呼叫來建立一個任務,在未來的某一個時間點,會有一個工作執行緒執行該任務。

public class Consumer implements Runnable {

    private final BusinessTask businessTask;
    private final BusinessLogic businessLogic;

    public Consumer(BusinessTask businessTask, BusinessLogic businessLogic) {
        this.businessTask = businessTask;
        this.businessLogic = businessLogic;
    }

    @Override
    public void run() {
        businessLogic.processTask(businessTask);
    }
}
@Component
public class ConsumerFactory {
    private final BusinessLogic businessLogic;

    public ConsumerFactory(BusinessLogic businessLogic) {
        this.businessLogic = businessLogic;
    }

    public Consumer newConsumer(BusinessTask businessTask) {
        return new Consumer(businessTask, businessLogic);
    }
}

最後,有一個 Producer 類,用於從資料庫中獲取資料並建立業務任務。在這個例子中,我們假定 fetchData() 是通過 scheduler 週期性呼叫的。

@Component
public class Producer {

    private final DataRepository dataRepository;
    private final ExecutorService executorService;
    private final ConsumerFactory consumerFactory;

    @Autowired
    public Producer(DataRepository dataRepository, ExecutorService executorService,
                    ConsumerFactory consumerFactory) {
        this.dataRepository = dataRepository;
        this.executorService = executorService;
        this.consumerFactory = consumerFactory;
    }

    public void fetchAndSubmitForProcessing() {
        List<Data> data = dataRepository.fetchNew();

        data.stream()
            // create a business task from data fetched from the database
            .map(BusinessTask::fromData)
            // create a consumer for each business task
            .map(consumerFactory::newConsumer)
            // submit the task for further processing in the future (submit is a non-blocking method)
            .forEach(executorService::submit);
    }
}

非常感謝 ExecutorService,這樣我們就可以集中精力實現業務邏輯,我們不需要擔心同步問題。上面的演示程式碼只用了一個生產者和一個消費者。但是,很容易擴充套件為多個生產者和多個消費者的情況。

總結

JDK 5 誕生於2004年,提供很多有用的併發工具,ExecutorService 類就是其中的一個。執行緒池通常應用於伺服器的底層(如 Tomcat 和 Undertow)。當然,執行緒池也不僅僅侷限於伺服器環境。在任何密集並行(embarrassingly parallel)難題中它們都非常有用。由於現在越來越多的軟體執行於多核系統上,執行緒池就更值得關注了。

相關文章