線程池(一)爲什麼要使用線程池

本文主要以實戰的方式,探索Java語言中的Thread與OS中進程(線程)的關係,探索OS創建線程的細節,並總結其中的資源消耗。
通過這個過程,你就知道爲什麼要使用線程池。

隨處可見的論調

在生產環境中,爲每個任務分配一個線程的做法存在一些缺陷,尤其當併發量很高的時候。

  • 創建和銷燬線程的代價相當高。
  • 活躍的線程會消耗系統資源,尤其是內存。
  • 穩定性難以保證。可創建線程的數量受多個條件限制,包括JVM的啓動參數、Thread構造函數中請求的棧大小以及底層操作系統的限制。

基於此,線程池的優勢就體現出來了。線程池是一種基於池化思想管理線程的工具,與String的字符串緩衝池、Integer的IntegerCache、數據庫的連接池等一樣,都是享元模式的典型應用。優點主要有以下三點:

  • 複用線程對象。節省了大量創建和銷燬線程的開銷。
  • 減少響應時間。每個任務被提交之後,通常狀況下,有創建好的線程執行它,不需要即時創建線程。
  • 統一管理線程

以上論調基本回答了本文的主題,爲什麼要使用線程池。可是隻有理論畢竟流於表面,我還有一個疑問。
Java中的線程和OS中的進程(線程)的對應關係是怎樣的?也就是,java中的線程和OS的線程是一對一還是多對一。很自然地,如果是多對一,大量java線程只需要少量OS線程支持,那麼資源消耗也許沒有太大。

實戰

一、驗證java線程與OS線程對應關係

實驗環境:

  • linux內核:3.1
  • CPU
    • 物理槽位:2
    • 每槽位核數:1
    • 每核線程數:1

在這裏插入圖片描述
在這裏插入圖片描述

實驗素材:

  • 一個計算密集型任務 : 求斐波那契數列的第n項。

        //遞歸,求斐波那契數列第n項
        public static long fib(int n){
            if(n == 1 || n == 2){
                return 1;
            }
            return fib(n-1) + fib(n-2);
        }
    
  • 下面的代碼只創建一個線程。該線程求解斐波那契數列的第50項。

    import java.io.IOException;
    
    public class CreateOneThread {
        public static void main(String[] args) throws IOException {
            System.out.println("BLOCKING...");
            System.in.read();//阻塞1
            
            Thread t1 = new Thread(() -> {
                long start = System.currentTimeMillis();
                System.out.println(fib(50));//求解斐波那契數列中第50個數
                System.out.println("total time:\t" + (System.currentTimeMillis() - start));
            });
    
            System.out.println("PRESS ENTER TO START...");
            System.in.read();//阻塞2
            t1.start();
        }
    }
    
  • 下面的代碼創建10k個線程。每一個線程求解斐波那契數列的第50項。

    
    import java.io.IOException;
    
    public class CreateMultiThreads {
        public static void main(String[] args) throws IOException {
            System.out.println("BLOCKING...");
            System.in.read();//阻塞1
    
            long start = System.currentTimeMillis();
            for(int i = 0; i < 10000; i++){
                Thread t1 = new Thread(() -> {
                    System.out.println(fib(50));
                });
                t1.start();
            }
            System.out.println("total time:\t" + (System.currentTimeMillis() - start));
        }
    }
    
  • 一個腳本,方便實驗過程中的操作

    rm -fr *out*
    /home/appsvr/jdk/jdk1.8*/bin/javac CreateOneThread.java
    strace -ff -o out /home/appsvr/jdk/jdk1.8*/bin/java CreateOneThread
    

    第一行刪除當前目錄下的所有含有“out”的文件,
    第二行編譯某java文件,
    第三行運行第二行編譯好的字節碼,並且用strace追蹤該進程執行過程中發生的系統調用、信號傳遞、進程狀態變更等等。其中,-ff 和 -o的聯合使用,使得每個進程的輸出都寫入到**out.{pid}**中。因此,通過查看當前目錄下out文件的數量可以非常直觀的看出啓動了幾個線程。

所有實驗素材羅列完畢。在兩個版本的代碼中,每個創建的線程都求解斐波那契數列第50項,這是一個CPU密集型任務,目的是讓每個線程都長時間保持運行or就緒狀態,一直參與CPU調度。
上述四份素材存在同一目錄下,讓我們看看截圖,當前目錄下只有這四個文件。
強調一下,整個實驗過程都在該目錄下進行
在這裏插入圖片描述

實驗過程:

整個實驗分n步。

  1. 執行mysh.sh --------->編譯並執行了 CreateOneThread.java
    CreateOneThread進程啓動了。
    程序阻塞在這一行代碼: System.in.read();//阻塞1

    在這裏插入圖片描述
    先讓它阻塞着,通過jps指令,查到CreateOneThread的進程id是28282
    我們看一下當前目錄,如下圖。
    在這裏插入圖片描述
    在這裏插入圖片描述可以看到當前目錄增加了12個out.pid文件,正是進程CreateOneThread所生成的。其中尾綴數字代表進程id。通過觀察這些out文件,發現12個進程之間的關係如上圖所示。即,28283是28282的子進程,28284 … 28293是28283的子進程。
    我們發現,操作系統內核通過系統調用 clone 創建子進程。下面粘貼一下進程28283創建28293的指令(出自out.28283),= 後面的返回值就是子進程ID。

    clone(child_stack=0x7fb6117d4fb0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|
    CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|
    CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, 
    parent_tidptr=0x7fb6117d59d0, 
    tls=0x7fb6117d5700, 
    child_tidptr=0x7fb6117d59d0) = 28293
    

    計算out.28283中“clone”的數量,發現有10個,這與28284~28293這十個子進程對應,進一步佐證了調一次clone創建一個進程
    在這裏插入圖片描述
    至此試驗第一步結束,總結如下:

    執行了mysh.sh,然後一通觀察與分析,主要是看out文件裏的system call,發現了每個子進程都是通過調用內核的clone來創建的。還有一點,此時的共12個進程,是CreateOneThread自身的,java代碼阻塞住了,還沒有執行到new Thread。這也是“阻塞1”的作用,讓我們可以先觀察到該進程自身的線程數。

  2. 第一步阻塞在這一行代碼:System.in.read();//阻塞1
    敲擊Enter鍵,程序開始往下執行,又阻塞在了這行代碼:

    System.in.read();//阻塞2
    

    在這裏插入圖片描述
    再次執行“ll -h”查看當前目錄所有文件,發現和第一步沒有差別;
    再次執行“grep clone out.28283 | wc -l”,結果還是10。
    由於這兩條指令的結果和第一步一致,此處不再貼圖。
    此阻塞處,已經新建了線程對象,但是還沒有調用該線程的start方法。
    OS沒有調用clone函數創建線程

    接下來再次敲擊Enter,該線程真正開始執行起來,計算斐波那契數列的第50項。

    再次執行“ll -h”和“grep clone out.28283 | wc -l”。發現當前目錄多了一個文件out.3482,說明新創建了一個進程,pid是3482,。而out.28283中clone數量也多了一個,變成了11。
    在這裏插入圖片描述
    在這裏插入圖片描述
    看一眼out.28283中增加的clone調用。正是返回了3482,由此可見進程3482和我們java代碼中創建的線程相對應。

    clone(child_stack=0x7fb6116d3fb0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|
    CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|
    CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID,
     parent_tidptr=0x7fb6116d49d0, 
     tls=0x7fb6116d4700,
      child_tidptr=0x7fb6116d49d0) = 3482
    

    在線程求解第50項的過程中,觀察對cpu使用率的監控,如下圖。1個線程在運行,12個在sleep,50.0us說明用戶態進程的CPU佔用率50%。這沒問題,因爲OS共有兩個邏輯CPU,我們只有一個線程在運行,因此另一個CPU閒着呢。一切都很合理!
    在這裏插入圖片描述
    最終java線程執行完畢,該CPU密集型任務,單線程執行共耗時52294ms。
    在這裏插入圖片描述
    第二步總結:

    第二步,代碼首先阻塞在調用start方法之前,此時OS沒有創建線程,而是在調用start之後創建了3482進程。說明操作系統爲java線程創建相對應的進程是在start方法調用之後。
    且證實了java代碼中start開啓一個線程之後,OS會調用系統調用clone創建一個進程與之對應。

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