使我短命1年的Java線程池

老大讓我把一個單線程的日播放量統計任務變成一個多線程的任務,時間由 2小時 優化到 30 分鐘以內,然後有了我與線程池死去活來的日子。

學習自:Java 併發編程 78 講

目錄

1 線程池的由來

2 線程池來了,它來了

1 線程池的由來

在 Java 誕生之初是沒有線程池的概念的,而是先有線程,隨着線程數的不斷增加,人們發現需要一個專門的類來管理它們,於是才誕生了線程池。沒有線程池的時候,每發佈一個任務就需要創建一個新的線程,這樣在任務少時是沒有問題的

/** 
* 描述:單個任務的時候,新建線程來執行 
*/ 
public class OneTask { 
 
    public static void main(String[] args) { 
        Thread thread0 = new Thread(new Task());
        thread0.start();
    } 
 
    static class Task implements Runnable { 
 
        public void run() { 
           System.out.println("Thread Name: " + Thread.currentThread().getName());
        } 
    } 
}

執行結果:

Thread Name: Thread-0

如圖,主線程調用 start() 方法,啓動了一個 t0 的子線程。這是在一個任務的場景下,隨着我們的任務增多,比如現在有 10 個任務了,那麼我們就可以使用 for 循環新建 10 個子線程

/** 
* 描述:for循環新建10個線程 
*/ 
public class TenTask { 
 
    public static void main(String[] args) { 
        for (int i = 0; i < 10; i++) { 
            Thread thread = new Thread(new Task());
            thread.start();
        } 
    } 
 
    static class Task implements Runnable { 
 
        public void run() { 
            System.out.println("Thread Name: " + Thread.currentThread().getName());
        } 
    } 
}

執行結果:

Thread Name: Thread-9
Thread Name: Thread-5
Thread Name: Thread-4
Thread Name: Thread-7
Thread Name: Thread-0
Thread Name: Thread-1
Thread Name: Thread-3
Thread Name: Thread-8
Thread Name: Thread-2
Thread Name: Thread-6

打印出來的順序是爲毛是錯亂的?比如 Thread-4 打印在了 Thread-3 之前,這是因爲,雖然  Thread-3 比  Thread-4 先執行 start 方法,但是這並不代表  Thread-3 就會先運行,運行的順序取決於線程調度器,有很大的隨機性。

主線程通過 for 循環創建了 t0~t9 這 10 個子線程,它們都可以正常的執行任務。如果此時我們的任務量突然飆升到 10000,我們就需要創建 10000 個子線程,創建線程時會產生系統開銷,並且每個線程還會佔用一定的內存等資源,會造成系統開銷大且浪費資源。因此創建線程的數量需要有一個上限,線程執行完需要被回收,大量的線程又會給垃圾回收帶來壓力。

總結:如果每個任務都創建一個線程會帶來哪些問題?

  • 反覆創建線程系統開銷比較大,每個線程創建和銷燬都需要時間,如果任務比較簡單,那麼就有可能導致創建和銷燬線程消耗的資源比線程執行任務本身消耗的資源還要大。
  • 過多的線程會佔用過多的內存等資源,還會帶來過多的上下文切換,同時還會導致系統不穩定

但我們的任務確實非常多,如果都在主線程串行執行,那效率也太低了,那應該怎麼辦呢?

  • 用一些固定的線程一直保持工作狀態並反覆執行任務
  • 根據需要創建線程,控制線程的總數量,避免佔用過多內存資源。

有個東西專門負責這些活就好了,於是乎。

2 線程池來了,它來了

線程池就好比一個池塘,池塘裏的水是有限且可控的,比如我們選擇線程數固定數量的線程池,假設線程池有 5 個線程,但此時的任務大於 5 個,線程池會讓餘下的任務進行排隊,而不是無限制的擴張線程數量,保障資源不會被過度消耗。如代碼所示,我們往 5 個線程的線程池中放入 10000 個任務並打印當前線程名字。

package com.wyd.thread;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/** 
 * 描述:用固定線程數的線程池執行10000個任務 
 */
public class ThreadPoolDemo {

    public static void main(String[] args) {
        // 創建線程數爲 5 的線程池
        ExecutorService pool = Executors.newFixedThreadPool(5);
        // 創建 10000 個任務交給線程池,線程池分配線程去執行
        for(int i=0; i<10000; i++){
            Task task = new Task();
            task.setNum(i);
            pool.execute(task);
        }
    }

    static class Task implements Runnable{
        private int num;

        @Override
        public void run() {
            System.out.println("ThreadName:" + Thread.currentThread().getName() + " is dealing with task " + num);
        }

        public int getNum() {
            return num;
        }

        public void setNum(int num) {
            this.num = num;
        }
    }
}

執行結果:

......

ThreadName:pool-1-thread-5 is dealing with task 9986
ThreadName:pool-1-thread-1 is dealing with task 9981
ThreadName:pool-1-thread-2 is dealing with task 9980
ThreadName:pool-1-thread-3 is dealing with task 9999
ThreadName:pool-1-thread-4 is dealing with task 9998

執行流程如圖所示,首先創建了一個線程池,線程池中有 5 個線程,然後線程池將 10000 個任務分配給這 5 個線程,這 5 個線程反覆領取任務並執行,直到所有任務執行完畢,這就是線程池的思想。

使用線程池比手動創建線程的好處:

  • 線程池可以解決線程生命週期的系統開銷問題,同時還可以加快響應速度。因爲線程池中的線程是可以複用的,我們只用少量的線程去執行大量的任務,這就大大減小了線程生命週期的開銷。而且線程通常不是等接到任務後再臨時創建,而是已經創建好時刻準備執行任務,這樣就消除了線程創建所帶來的延遲,提升了響應速度,增強了用戶體驗。
  • 線程池可以統籌內存和 CPU 的使用,避免資源使用不當。線程池會根據配置和任務數量靈活地控制線程數量,不夠的時候就創建,太多的時候就回收,避免線程過多導致內存溢出,或線程太少導致 CPU 資源浪費,達到了一個完美的平衡。
  • 線程池可以統一管理資源。比如線程池可以統一管理任務隊列和線程,可以統一開始或結束任務,比單個線程逐一處理任務要更方便、更易於管理,同時也有利於數據統計,比如我們可以很方便地統計出已經執行過的任務的數量。

好像命還挺長,繼續下一篇:使我短命2年的Java線程池

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