本文主要以實戰的方式,探索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步。
-
執行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”的作用,讓我們可以先觀察到該進程自身的線程數。
-
第一步阻塞在這一行代碼: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創建一個進程與之對應。