線程與線程池的那些事之線程篇

本文關鍵字:java

線程線程池單線程多線程線程池的好處線程回收建立方式核心參數底層機制拒絕策略,參數設置,動態監控線程隔離c++

線程和線程池相關的知識,是Java學習或者面試中必定會遇到的知識點,本篇咱們會從線程和進程,並行與併發,單線程和多線程等,一直講解到線程池,線程池的好處,建立方式,重要的核心參數,幾個重要的方法,底層實現,拒絕策略,參數設置,動態調整,線程隔離等等。主要的大綱以下(本文只涉及線程部分,線程池下篇講):git

進程和線程

從線程到進程

要說線程池,就不得不先講講線程,什麼是線程?github

線程(英語:thread)是 操做系統可以進行運算 調度的最小單位。它被包含在 進程之中,是 進程中的實際運做單位。

那麼問題來了,進程又是什麼?面試

進程是操做系統中進行保護和資源分配的基本單位。

是否是有點懵,進程摸得着看得見麼?具體怎麼表現?打開Windows的任務管理器或者Mac的活動監視器,就能夠看到,基本每個打開的App就是一個進程,可是並非必定的,一個應用程序可能存在多個進程redis

好比下面的Typora就顯示了兩個進程,每一個進程後面有一個PID是惟一的標識,也是由系統分配的。除此以外,每一個進程均可以看到有多少個線程在執行,好比微信有32個線程在執行。重要的一句話:一個程序運行以後至少有一個進程,一個進程能夠包含多個線程。編程

<img src="https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/image-20210508225417275.png" alt="image-20210508225417275" style="zoom:50%;" />瀏覽器

爲何須要進程?

程序,就是指令的集合,指令的集合說白了就是文件,讓程序跑起來,在執行的程序,纔是進程。程序是靜態的描述文本,而進程是程序的一次執行活動,是動態的。進程是擁有計算機分配的資源的運行程序。緩存

咱們不可能一個計算機只有一個進程,就跟咱們全國不可能只有一個市或者一個部門,計算機是一個龐然大物,裏面的運轉須要有條理,就須要按照功能劃分出比較獨立的單位,分開管理。每一個進程有本身的職責,也有本身的獨立內存空間,不可能混着使用,要是全部的程序共用一個進程就會亂套。微信

每一個進程,都有各自獨立的內存,進程之間內存地址隔離,進程的資源,好比:代碼段,數據集,堆等等,還可能包括一些打開的文件或者信號量,這都是每一個進程本身的數據。同時,因爲進程的隔離性,即便有一個程序的進程出現問題了,通常不會影響到其餘的進程的使用。

進程在Linux系統中,進程有一個比較重要的東西,叫進程控制塊(PCB),僅作了解:

PCB是進程的惟一標識,由鏈表實現,是爲了動態的插入以及刪除,建立進程的時候,生成一個PCB,進程結束的時候,回收這個PCBPCB主要包括如下的信息:

  • 進程狀態
  • 進程標識信息
  • 定時器
  • 用戶可見的寄存器,控制狀態寄存區,棧指針等等。

進程怎麼切換的呢?

先明白計算機裏面的一個事實:CPU運轉得超級無敵快,快到其餘的只有寄存器差很少能匹配它的速度,可是不少時候咱們須要從磁盤或者內存讀或者寫數據,這些設備的速度太慢了,與之相差太遠。(若是不特殊說明,默認是單核的CPU

假設一個程序/進程的任務執行一段時間,要寫磁盤,寫磁盤不須要CUP進行計算,那CPU就空出來了,可是其餘的程序也不能用,CPU就乾等着,等到寫完磁盤再接着執行。這多浪費,CPU又不是這個程序一家的,其餘的應用也要使用。CPU你不用的時候,總有別人須要用。

因此CPU資源須要調度,程序A不用的時候,能夠切出來,讓程序B去使用,可是程序A切回來的時候怎麼保證它可以接着以前的位置繼續執行呢?這時候不得不提上下文的事。

當程序A(假設爲單進程)放棄CPU的時候,須要保存當前的上下文,何爲上下文?也就是除了CPU以外,寄存器或者其餘的狀態,就跟犯罪現場同樣,須要拍個照,要不到時候別的程序執行完以後,怎麼知道接下來怎麼執行程序A,以前執行到哪一步了。總結一句話:保存當前程序的執行狀態。

上下文切換通常還涉及緩存的開銷,也就是緩存會失效,通常執行的時候,CPU會緩存一些數據方便下次更快的執行,一旦進行上下文切換,原來的緩存就失效了,須要從新緩存。

調度通常有兩種(通常是按照線程維度來調度),CPU的時間被分爲特別小的時間片:

  • 分時調度:每一個線程或者進程輪流的使用CPU,平均時間分配到每一個線程或者進程。
  • 搶佔式調度:優先級高的線程/進程當即搶佔下一個時間片,若是優先級相同,那麼隨機選擇一個進程。

時間片超級短,CPU超級快,給咱們無比絲滑的感受,就像是多個任務在同時進行

咱們如今操做系統或者其餘的系統,基本都是搶佔式調度,爲何?

由於若是使用分時調度,很難作到實時響應,當後臺的聊天程序在進行網絡傳輸的時候,分配予它的時間片尚未使用完,那我點擊瀏覽器,是沒有辦法實時響應的。除此以外,若是前面的進程掛了,可是一直佔有CPU,那麼後面的任務將永遠得不到執行。

因爲CPU的處理能力超級快,就算是單核的CPU,運行着多個程序,多個進程,通過搶佔式的調度,每個程序使用的時候都像是獨享了CPU同樣順滑。進程有效的提升了CPU的使用率,可是進程在上下文切換的時候是存在着必定的成本的。

線程和進程什麼關係?

前面說了進程,那有了進程,爲啥還要線程,多個應用程序,假設咱們每一個應用程序要作n件事,就用n個進程不行麼?

能夠,可是不必。

進程通常由程序,數據集合和進程控制塊組成,同一個應用程序通常是須要使用同一個數據空間的,要是一個應用程序搞不少個進程,就算有能力作到數據空間共享,進程的上下文切換都會消耗不少資源。(通常一個應用程序不會有不少進程,大多數一個,少數有幾個)

進程的顆粒度比較大,每次執行都須要上下文切換,若是同一個程序裏面的代碼段ABC,作不同的東西,若是分給多個進程去處理,那麼每次執行都有切換進程上下文。這太慘了。一個應用程序的任務是一家人,住在同一個屋子下(同一個內存空間),有必要每一個房間都當成每一戶,去派出所登記成一個戶口麼?

進程缺點:

  • 信息共享難,空間獨立
  • 切換須要fork(),切換上下文,開銷大
  • 只能在一個時間點作一件事
  • 若是進程阻塞了,要等待網絡傳過來數據,那麼其餘不依賴這個數據的任務也作不了

可是有人會說,那我一個應用程序有不少事情要作,總不能只用一個進程,全部事情都等着它來處理啊?那不是會阻塞住麼?

確實啊,單獨一個進程處理不了問題,那麼咱們把進程分得更小,裏面分紅不少線程,一家人,每一個人都有本身的事情作,那咱們每一個人就是一個線程,一家人就是一個進程,這樣豈不是更好麼?

進程是描述CPU時間片調度的時間片斷,可是線程是更細小的時間片斷,二者的顆粒度不同。線程能夠稱爲輕量級的進程。其實,線程也不是一開始就有的概念,而是隨着計算機發展,對多個任務上下文切換要求愈來愈高,隨之抽象出來的概念。
$$進程時間段 = CPU加載程序上下文的時間 + CPU執行時間 + CPU保存程序上下文的時間$$

$$ 線程時間段 = CPU加載線程上下文的時間 + CPU執行時間 + CPU保存線程上下文的時間$$ **最重要的是,進程切換上下文的時間遠比線程切換上下文的時間成本要高**,若是是同一個進程的不一樣線程之間搶佔到`CPU`,切換成本會比較低,由於他們**共享了進程的地址空間**,線程間的通訊容易不少,經過共享進程級全局變量便可實現。 何況,如今多核的處理器,讓不一樣進程在不一樣核上跑,進程內的線程在同個核上作切換,儘可能減小(不能夠避免)進程的上下文切換,或者讓不一樣線程跑在不一樣的處理器上,進一步提升效率。 進程和線程的模型以下: ![image-20210509163642149](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/image-20210509163642149.png) ### 線程和進程的區別或者優勢 - 線程是程序執行的最小單位,進程是操做系統分配資源的最小單位。 - 一個應用可能多個進程,一個進程由一個或者多個線程組成 - 進程相互獨立,通訊或者溝通成本高,在同一個進程下的線程共享進程的內存等,相互之間溝通或者協做成本低。 - 線程切換上下文比進程切換上下文要快得多。 ## 線程有哪些狀態 如今咱們所說的是`Java`中的線程`Thread`,一個線程在一個給定的時間點,只能處於一種狀態,這些狀態都是虛擬機的狀態,不能反映任何操做系統的線程狀態,一共有六種/七種狀態: - `NEW`:建立了線程對象,可是尚未調用`Start()`方法,尚未啓動的線程處於這種狀態。 - `Running`:運行狀態,其實包含了兩種狀態,可是`Java`線程將就緒和運行中統稱爲可運行 - `Runnable`:就緒狀態:建立對象後,調用了`start()`方法,該狀態的線程還位於可運行線程池中,等待調度,獲取`CPU`的使用權 - 只是有資格執行,不必定會執行 - `start()`以後進入就緒狀態,`sleep()`結束或者`join()`結束,線程得到對象鎖等都會進入該狀態。 - `CPU`時間片結束或者主動調用`yield()`方法,也會進入該狀態 - `Running` :獲取到`CPU`的使用權(得到CPU時間片),變成運行中 - `BLOCKED` :阻塞,線程阻塞於鎖,等待監視器鎖,通常是`Synchronize`關鍵字修飾的方法或者代碼塊 - `WAITING` :進入該狀態,須要等待其餘線程通知(`notify`)或者中斷,一個線程無限期地等待另外一個線程。 - `TIMED_WAITING` :超時等待,在指定時間後自動喚醒,返回,不會一直等待 - `TERMINATED` :線程執行完畢,已經退出。若是已終止再調用start(),將會拋出`java.lang.IllegalThreadStateException`異常。 ![image-20210509224848865](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/image-20210509224848865.png) 能夠看到`Thread.java`裏面有一個`State`枚舉類,枚舉了線程的各類狀態(`Java`線程將**就緒**和**運行中**統稱爲**可運行**): ```Java public enum State { /** * 還沒有啓動的線程的線程狀態。 */ NEW, /** * 可運行線程的線程狀態,一個處於可運行狀態的線程正在Java虛擬機中執行,但它可能正在等待來自操做系統(如處理器)的其餘資源。 */ RUNNABLE, /** * 等待監視器鎖而阻塞的線程的線程狀態。 * 處於阻塞狀態的線程正在等待一個監視器鎖進入一個同步的塊/方法,或者在調用Oject.wait()方法以後從新進入一個同步代碼塊 */ BLOCKED, /** * 等待線程的線程狀態,線程因爲調用其中一個線程而處於等待狀態 */ WAITING, /** * 具備指定等待時間的等待線程的線程狀態,線程因爲調用其中一個線程而處於定時等待狀態。 */ TIMED_WAITING, /** * 終止線程的線程狀態,線程已經完成執行。 */ TERMINATED; } ``` 除此以外,Thread類還有一些屬性是和線程對象有關的: - long tid:線程序號 - char name[]:線程名稱 - int priority:線程優先級 - boolean daemon:是否守護線程 - Runnable target:線程須要執行的方法 介紹一下上面圖中講解到線程的幾個重要方法,它們都會致使線程的狀態發生一些變化: - `Thread.sleep(long)`:調用以後,線程進入`TIMED_WAITING`狀態,可是不會釋放對象鎖,到時間甦醒後進入`Runnable`就緒狀態 - `Thread.yield()`:線程調用該方法,表示放棄獲取的`CPU`時間片,可是不會釋放鎖資源,一樣變成就緒狀態,等待從新調度,不會阻塞,可是也不能保證必定會讓出`CPU`,極可能又被從新選中。 - `thread.join(long)`:當前線程調用其餘線程`thread`的`join()`方法,當前線程不會釋放鎖,會進入`WAITING`或者`TIMED_WAITING`狀態,等待thread執行完畢或者時間到,當前線程進入就緒狀態。 - `object.wait(long)`:當前線程調用對象的`wait()`方法,當前線程會釋放得到的對象鎖,進入等待隊列,`WAITING`,等到時間到或者被喚醒。 - `object.notify()`:喚醒在該對象監視器上等待的線程,隨機挑一個 - `object.notifyAll()`:喚醒在該對象監視器上等待的全部線程 ## 單線程和多線程 單線程,就是隻有一條線程在執行任務,串行的執行,而多線程,則是多條線程同時執行任務,所謂同時,並非必定真的同時,若是在單核的機器上,就是假同時,只是看起來同時,其實是輪流佔據CPU時間片。 下面的每個格子是一個時間片(每個時間片實際上超級無敵短),不一樣的線程其實能夠搶佔不一樣的時間片,得到執行權。**時間片分配的單位是線程,而不是進程,進程只是容器** ![image-20210511002923132](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/image-20210511002923132.png) ### 如何啓動一個線程 其實`Java`的`main()`方法本質上就啓動了一個線程,可是**本質上不是隻有一個線程**,看結果的 5 就大體知道,其實一共有 5 個線程,主線程是第 5 個,大可能是**後臺線程**: ```java public class Test { public static void main(String[] args) { System.out.println(Thread.currentThread().toString()); } } ``` 執行結果: ```txt Thread[main,5,main] ``` 能夠看出上面的線程是`main`線程,可是要想建立出有別於`main`線程的方式,有四種: - 自定義類去實現`Runnable`接口 - 繼承`Thread`類,重寫`run()`方法 - 經過`Callable`和`FutureTask`建立線程 - 線程池直接啓動(本質上不算是) #### 實現Runnable接口 ```java class MyThread implements Runnable{ @Override public void run(){ System.out.println("Hello world"); } } public class Test { public static void main(String[] args) { Thread thread = new Thread(new MyThread()); thread.start(); System.out.println("Main Thread"); } } ``` 運行結果: ```txt Main Thread Hello world ``` 若是看底層就能夠看到,構造函數的時候,咱們將`Runnable`的實現類對象傳遞進入,會將`Runnable`實現類對象保存下來: ```java public Thread(Runnable target) { this(null, target, "Thread-" + nextThreadNum(), 0); } ``` 而後再調用`start()`方法的時候,會調用原生的`start0()`方法,原生方法是由`c`或者`c++`寫的,這裏看不到具體的實現: ```java public synchronized void start() { if (threadStatus != 0) throw new IllegalThreadStateException(); group.add(this); boolean started = false; try { // 正式的調用native原生方法 start0(); started = true; } finally { try { if (!started) { group.threadStartFailed(this); } } catch (Throwable ignore) { } } } ``` `Start0()`在底層確實調用了`run()`方法,而且不是直接調用的,而是啓用了另一個線程進行調用的,這一點在代碼註釋裏面寫得比較清楚,在這裏咱們就不展開講,咱們將關注點放到`run()`方法上,調用的就是剛剛那個`Runnable`實現類的對象的`run()`方法: ```java @Override public void run() { if (target != null) { target.run(); } } ``` #### 繼承Thread類 因爲`Thread`類自己就實現了`Runnable`接口,因此咱們只要繼承它就能夠了: ```java class Thread implements Runnable { } ``` 繼承以後重寫run()方法便可: ```java class MyThread extends Thread{ @Override public void run(){ System.out.println("Hello world"); } } public class Test { public static void main(String[] args) { Thread thread = new Thread(new MyThread()); thread.start(); System.out.println("Main Thread"); } } ``` 執行結果和上面的同樣,其實兩種方式本質上都是同樣的,一個是實現了`Runnable`接口,另一個是繼承了實現了`Runnable`接口的`Thread`類。兩種都沒有返回值,由於`run()`方法的返回值是`void`。 #### Callable和FutureTask建立線程 要使用該方式,按照如下步驟: - 建立`Callable`接口的實現類,實現`call()`方法 - 建立`Callable`實現類的對象實例,用`FutureTask`包裝Callable的實現類實例,包裝成`FutureTask`的實例,`FutureTask`的實例封裝了`Callable`對象的`Call()`方法的返回值 - 使用`FutureTask`對象做爲`Thread`對象的`target`建立並啓動線程,`FutureTask`實現了`RunnableFuture`,`RunnableFuture`繼承了`Runnable` - 調用`FutureTask`對象的`get()`來獲取子線程執行結束的返回值 ```java import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; public class CallableTest { public static void main(String[] args) throws Exception{ Callable<String> callable = new MyCallable<String>(); FutureTask<String> task = new FutureTask<String>(callable); Thread thread = new Thread(task); thread.start(); System.out.println(Thread.currentThread().getName()); System.out.println(task.get()); } } class MyCallable<String> implements Callable<String> { @Override public String call() throws Exception { System.out.println( Thread.currentThread().getName() + " Callable Thread"); return (String) "Hello"; } } ``` 執行結果: ```txt main Thread-0 Callable Thread Hello ``` 其實這種方式本質上也是`Runnable`接口來實現的,只不過作了一系列的封裝,可是不一樣的是,它能夠實現返回值,若是咱們期待一件事情能夠經過另一個線程來獲取結果,可是可能須要消耗一些時間,好比異步網絡請求,其實能夠考慮這種方式。 `Callable`和`FutureTask`是後面才加入的功能,是爲了適應多種併發場景,`Callable`和`Runnable`的區別以下: - `Callable` 定義方法是`call()`,`Runnable`定義的方法是`run()` - `Callable`的`call()`方法有返回值,`Runnable`的`run()`方法沒有返回值 - `Callable`的`call()`方法能夠拋出異常,`Runnable`的`run()`方法不能拋出異常 #### 線程池啓動線程 本質上也是經過實現`Runnable`接口,而後放到線程池中進行執行: ```java import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; class MyThread extends Thread { @Override public void run() { System.out.println(Thread.currentThread().getName() + " : hello world"); } } public class Test { public static void main(String[] args) throws InterruptedException { ExecutorService executorService = Executors.newFixedThreadPool(5); for (int i = 0; i < 10; i++) { MyThread thread = new MyThread(); executorService.execute(thread); } executorService.shutdown(); } } ``` 執行結果以下,能夠看到五個核心線程一直在執行,沒有規律,循環十次,可是並無建立出十個線程,這和線程池的設計以及參數有關,後面會講解: ```txt pool-1-thread-5 : hello world pool-1-thread-4 : hello world pool-1-thread-5 : hello world pool-1-thread-3 : hello world pool-1-thread-2 : hello world pool-1-thread-1 : hello world pool-1-thread-2 : hello world pool-1-thread-3 : hello world pool-1-thread-5 : hello world pool-1-thread-4 : hello world ``` 總結一下,啓動一個線程,其實本質上都離不開`Runnable`接口,無論是繼承仍是實現接口。 ### 多線程可能帶來的問題 - 消耗資源:上下文切換,或者建立以及銷燬線程,都是比較消耗資源的。 - 競態條件:多線程訪問或者修改同一個對象,假設自增操做`num++`,操做分爲三步,讀取`num`,`num`加1,寫回`num`,並不是原子操做,那麼多個線程之間交叉運行,就會產生不如預期的結果。 - 內存的可見性:每一個線程都有本身的內存(緩存),通常修改的值都放在本身線程的緩存上,到刷新至主內存有必定的時間,因此可能一個線程更新了,可是另一個線程獲取到的仍是久的值,這就是不可見的問題。 - 執行順序難預知:線程先`start()`不必定先執行,是由系統決定的,會致使共享的變量或者執行結果錯亂 ## 併發與並行 併發是指兩個或多個事件在同一時間間隔發生,好比在同`1s`中內計算機不只計算`數據1`,同時也計算了`數據2`。可是兩件事情可能在某一個時刻,不是真的同時進行,極可能是搶佔時間片就執行,搶不到就別人執行,可是因爲時間片很短,因此在1s中內,看似是同時執行完成了。固然前面說的是單核的機器,併發不是真的同時執行,可是多核的機器上,併發也多是真的在同時執行,只是有可能,這個時候的併發也叫作並行。 ![image-20210511012516227](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/image-20210511012516227.png) 並行是指在同一時刻,有多條指令在多個處理器上同時執行,真正的在同時執行。 ![image-20210511012723433](https://markdownpicture.oss-cn-qingdao.aliyuncs.com/blog/image-20210511012723433.png) 若是是單核的機器,最多隻能併發,不可能並行處理,只能把CPU運行時間分片,分配給各個線程執行,執行不一樣的線程任務的時候須要上下文切換。而多核機器,能夠作到真的並行,同時在多個核上計算,運行。**並行操做必定是併發的,可是併發的操做不必定是並行的。** ### 關於做者 秦懷,公衆號【**秦懷雜貨店**】做者,技術之路不在一時,山高水長,縱使緩慢,馳而不息。我的寫做方向:Java源碼解析,JDBC,Mybatis,Spring,redis,分佈式,劍指Offer,LeetCode等,認真寫好每一篇文章,不喜歡標題黨,不喜歡花裏胡哨,大多寫系列文章,不能保證我寫的都徹底正確,可是我保證所寫的均通過實踐或者查找資料。遺漏或者錯誤之處,還望指正。 [2020年我寫了什麼?](http://aphysia.cn/archives/2020) [開源編程筆記](https://damaer.github.io/Coding/#/) [150頁的劍指Offer PDF領取](https://mp.weixin.qq.com/s?__biz=MzA3NTUwNzk0Mw==&tempkey=MTExNF9zZ2FPelJtWkNCdlZ6dTRuVThBSDdNc01JNFZuSTBrVlZWU0dCRk45dzlLVmx3SWx3NXlHVE5DWkRTSFBnNWVhRFV6RkNKOURjSmhUTExZeVp4QndwbEZ4Q2NfWUlzMzI2bDQzSm51TVJ4SE14QVhsUFIxSWJkcWtGQVhhLVVwZGRPZ0cwRHFDaGJvZ2pPeDM3NXdzcGF5N3A5bFdRaE9JU1Rpbi1Rfn4%3D&chksm=383018090f47911fd2458fe7c2ee89cbde7a7875dcba06d9f2e4daca191c7c0ab6409777f14d#rd)

相關文章
相關標籤/搜索