天天看點

深入了解 Java 多線程核心知識:跳槽面試必備概念梳理線程的生命周期線程的優先級為什麼要用線程池ThreadPoolExecutorExecutors概念梳理線程的生命周期線程的優先級為什麼要用線程池ThreadPoolExecutorExecutors

多線程相對于其他 Java 知識點來講,有一定的學習門檻,并且了解起來比較費勁。在平時工作中如若使用不當會出現資料錯亂、執行效率低(還不如單線程去運作)或者死鎖程式挂掉等等問題,是以掌握了解多線程至關重要。

本文從基礎概念開始到最後的并發模型由淺入深,講解下線程方面的知識。

概念梳理

本節我将帶大家了解多線程中幾大基礎概念。

并發與并行

并行,表示兩個線程同時做事情。

并發,表示一會做這個事情,一會做另一個事情,存在着排程。單核 CPU 不可能存在并行(微觀上)。

深入了解 Java 多線程核心知識:跳槽面試必備概念梳理線程的生命周期線程的優先級為什麼要用線程池ThreadPoolExecutorExecutors概念梳理線程的生命周期線程的優先級為什麼要用線程池ThreadPoolExecutorExecutors

臨界區

臨界區用來表示一種公共資源或者說是共享資料,可以被多個線程使用。但是每一次,隻能有一個線程使用它,一旦臨界區資源被占用,其他線程要想使用這個資源,就必須等待。

深入了解 Java 多線程核心知識:跳槽面試必備概念梳理線程的生命周期線程的優先級為什麼要用線程池ThreadPoolExecutorExecutors概念梳理線程的生命周期線程的優先級為什麼要用線程池ThreadPoolExecutorExecutors

阻塞與非阻塞

阻塞和非阻塞通常用來形容多線程間的互相影響。比如一個線程占用了臨界區資源,那麼其它所有需要這個資源的線程就必須在這個臨界區中進行等待,等待會導緻線程挂起。這種情況就是阻塞。

此時,如果占用資源的線程一直不願意釋放資源,那麼其它所有阻塞在這個臨界區上的線程都不能工作。阻塞是指線程在作業系統層面被挂起。阻塞一般性能不好,需大約8萬個時鐘周期來做排程。

非阻塞則允許多個線程同時進入臨界區。

死鎖

死鎖是程序死鎖的簡稱,是指多個程序循環等待他方占有的資源而無限的僵持下去的局面。

深入了解 Java 多線程核心知識:跳槽面試必備概念梳理線程的生命周期線程的優先級為什麼要用線程池ThreadPoolExecutorExecutors概念梳理線程的生命周期線程的優先級為什麼要用線程池ThreadPoolExecutorExecutors

活鎖

假設有兩個線程1、2,它們都需要資源 A/B,假設1号線程占有了 A 資源,2号線程占有了 B 資源;由于兩個線程都需要同時擁有這兩個資源才可以工作,為了避免死鎖,1号線程釋放了 A 資源占有鎖,2号線程釋放了 B 資源占有鎖;此時 AB 空閑,兩個線程又同時搶鎖,再次出現上述情況,此時發生了活鎖。

簡單類比,電梯遇到人,一個進的一個出的,對面占路,兩個人同時往一個方向讓路,來回重複,還是堵着路。

如果線上應用遇到了活鎖問題,恭喜你中獎了,這類問題比較難排查。

饑餓

饑餓是指某一個或者多個線程因為種種原因無法獲得所需要的資源,導緻一直無法執行。

線程的生命周期

線上程的生命周期中,它要經曆建立、可運作、不可運作幾種狀态。

建立狀态

當用 new 操作符建立一個新的線程對象時,該線程處于建立狀态。

處于建立狀态的線程隻是一個空的線程對象,系統不為它配置設定資源。

可運作狀态

執行線程的 start() 方法将為線程配置設定必須的系統資源,安排其運作,并調用線程體——run()方法,這樣就使得該線程處于可運作狀态(Runnable)。

這一狀态并不是運作中狀态(Running),因為線程也許實際上并未真正運作。

不可運作狀态

當發生下列事件時,處于運作狀态的線程會轉入到不可運作狀态:

調用了 sleep() 方法;

線程調用 wait() 方法等待特定條件的滿足;

線程輸入/輸出阻塞;

傳回可運作狀态;

處于睡眠狀态的線程在指定的時間過去後;

如果線程在等待某一條件,另一個對象必須通過 notify() 或 notifyAll() 方法通知等待線程條件的改變;

如果線程是因為輸入輸出阻塞,等待輸入輸出完成。

線程的優先級

線程優先級及設定

線程的優先級是為了在多線程環境中便于系統對線程的排程,優先級高的線程将優先執行。一個線程的優先級設定遵從以下原則:

線程建立時,子繼承父的優先級;

線程建立後,可通過調用 setPriority() 方法改變優先級;

線程的優先級是1-10之間的正整數。

線程的排程政策

線程排程器選擇優先級最高的線程運作。但是,如果發生以下情況,就會終止線程的運作:

線程體中調用了 yield() 方法,讓出了對 CPU 的占用權;

線程體中調用了 sleep() 方法,使線程進入睡眠狀态;

線程由于 I/O 操作而受阻塞;

另一個更高優先級的線程出現;

在支援時間片的系統中,該線程的時間片用完。

單線程建立方式

單線程建立方式比較簡單,一般隻有兩種方式:繼承 Thread 類和實作 Runnable 接口;這兩種方式比較常用就不在 Demo 了,但是對于新手需要注意的問題有:

不管是繼承 Thread 類還是實作 Runable 接口,業務邏輯是寫在 run 方法裡面,線程啟動的時候是執行 start() 方法;

開啟新的線程,不影響主線程的代碼執行順序也不會阻塞主線程的執行;

新的線程和主線程的代碼執行順序是不能夠保證先後的;

對于多線程程式,從微觀上來講某一時刻隻有一個線程在工作,多線程目的是讓 CPU 忙起來;

通過檢視 Thread 的源碼可以看到,Thread 類是實作了 Runnable 接口的,是以這兩種本質上來講是一個;

PS:平時在工作中也可以借鑒這種代碼結構,對上層調用來講提供更多的選擇,作為服務提供方核心業務歸一維護

為什麼要用線程池

通過上面的介紹,完全可以開發一個多線程的程式,為什麼還要引入線程池呢。主要是因為上述單線程方式存在以下幾個問題:

線程的工作周期:線程建立所需時間為 T1,線程執行任務所需時間為 T2,線程銷毀所需時間為 T3,往往是 T1+T3 大于 T2,所有如果頻繁建立線程會損耗過多額外的時間;

如果有任務來了,再去建立線程的話效率比較低,如果從一個池子中可以直接擷取可用的線程,那效率會有所提高。是以線程池省去了任務過來,要先建立線程再去執行的過程,節省了時間,提升了效率;

線程池可以管理和控制線程,因為線程是稀缺資源,如果無限制的建立,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一的配置設定,調優和監控;

線程池提供隊列,存放緩沖等待執行的任務。

大緻總結了上述的幾個原因,是以可以得出一個結論就是在平時工作中,如果要開發多線程程式,盡量要使用線程池的方式來建立和管理線程。

通過線程池建立線程從調用 API 角度來說分為兩種,一種是原生的線程池,另外該一種是通過 Java 提供的并發包來建立,後者比較簡單,後者其實是對原生的線程池建立方式做了一次簡化包裝,讓調用者使用起來更友善,但道理都是一樣的。是以搞明白原生線程池的原理是非常重要的。

ThreadPoolExecutor

通過 ThreadPoolExecutor 建立線程池,API 如下所示:

publicThreadPoolExecutor(intcorePoolSize,intmaximumPoolSize,longkeepAliveTime,                          TimeUnit unit,                          BlockingQueue workQueue);

先來解釋下其中的參數含義(如果看的比較模糊可以大緻有個印象,後面的圖是關鍵)。

corePoolSize

核心池的大小。

在建立了線程池後,預設情況下,線程池中并沒有任何線程,而是等待有任務到來才建立線程去執行任務,除非調用了 prestartAllCoreThreads() 或者 prestartCoreThread() 方法,從這兩個方法的名字就可以看出,是預建立線程的意思,即在沒有任務到來之前就建立 corePoolSize 個線程或者一個線程。預設情況下,在建立了線程池後,線程池中的線程數為0,當有任務來之後,就會建立一個線程去執行任務,當線程池中的線程數目達到 corePoolSize 後,就會把到達的任務放到緩存隊列當中。

maximumPoolSize

線程池最大線程數,這個參數也是一個非常重要的參數,它表示線上程池中最多能建立多少個線程。

keepAliveTime

表示線程沒有任務執行時最多保持多久時間會終止。預設情況下,隻有當線程池中的線程數大于 corePoolSize 時,keepAliveTime 才會起作用,直到線程池中的線程數不大于 corePoolSize,即當線程池中的線程數大于 corePoolSize 時,如果一個線程空閑的時間達到 keepAliveTime,則會終止,直到線程池中的線程數不超過 corePoolSize。

但是如果調用了 allowCoreThreadTimeOut(boolean) 方法,線上程池中的線程數不大于 corePoolSize 時,keepAliveTime 參數也會起作用,直到線程池中的線程數為0。

unit

參數 keepAliveTime 的時間機關。

workQueue

一個阻塞隊列,用來存儲等待執行的任務,這個參數的選擇也很重要,會對線程池的運作過程産生重大影響,一般來說,這裡的阻塞隊列有以下這幾種選擇:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue。

threadFactory

線程工廠,主要用來建立線程。

handler

表示當拒絕處理任務時的政策,有以下四種取值:

ThreadPoolExecutor.AbortPolicy:丢棄任務并抛出 RejectedExecutionException 異常;

ThreadPoolExecutor.DiscardPolicy:也是丢棄任務,但是不抛出異常;

ThreadPoolExecutor.DiscardOldestPolicy:丢棄隊列最前面的任務,然後重新嘗試執行任務(重複此過程);

ThreadPoolExecutor.CallerRunsPolicy:由調用線程處理該任務。

上面這些參數是如何配合工作的呢?請看下圖:

深入了解 Java 多線程核心知識:跳槽面試必備概念梳理線程的生命周期線程的優先級為什麼要用線程池ThreadPoolExecutorExecutors概念梳理線程的生命周期線程的優先級為什麼要用線程池ThreadPoolExecutorExecutors

注意圖上面的序号。

簡單總結下線程池之間的參數協作分為以下幾步:

線程優先向 CorePool 中送出;

在 Corepool 滿了之後,線程被送出到任務隊列,等待線程池空閑;

在任務隊列滿了之後 corePool 還沒有空閑,那麼任務将被送出到 maxPool 中,如果 MaxPool 滿了之後執行 task 拒絕政策。

流程圖如下:

深入了解 Java 多線程核心知識:跳槽面試必備概念梳理線程的生命周期線程的優先級為什麼要用線程池ThreadPoolExecutorExecutors概念梳理線程的生命周期線程的優先級為什麼要用線程池ThreadPoolExecutorExecutors

image

以上就是原生線程池建立的核心原理。除了原生線程池之外并發包還提供了簡單的建立方式,上面也說了它們是對原生線程池的一種包裝,可以讓開發者簡單快捷的建立所需要的線程池。

Executors

newSingleThreadExecutor

建立一個線程的線程池,在這個線程池中始終隻有一個線程存在。如果線程池中的線程因為異常問題退出,那麼會有一個新的線程來替代它。此線程池保證所有任務的執行順序按照任務的送出順序執行。

newFixedThreadPool

建立固定大小的線程池。每次送出一個任務就建立一個線程,直到線程達到線程池的最大大小。線程池的大小一旦達到最大值就會保持不變,如果某個線程因為執行異常而結束,那麼線程池會補充一個新線程。

newCachedThreadPool

可根據實際情況,調整線程數量的線程池,線程池中的線程數量不确定,如果有空閑線程會優先選擇空閑線程,如果沒有空閑線程并且此時有任務送出會建立新的線程。在正常開發中并不推薦這個線程池,因為在極端情況下,會因為 newCachedThreadPool 建立過多線程而耗盡 CPU 和記憶體資源。

newScheduledThreadPool

此線程池可以指定固定數量的線程來周期性的去執行。比如通過 scheduleAtFixedRate 或者 scheduleWithFixedDelay 來指定周期時間。

PS:另外在寫定時任務時(如果不用 Quartz 架構),最好采用這種線程池來做,因為它可以保證裡面始終是存在活的線程的。

推薦使用 ThreadPoolExecutor 方式

在阿裡的 Java 開發手冊時有一條是不推薦使用 Executors 去建立,而是推薦去使用 ThreadPoolExecutor 來建立線程池。

這樣做的目的主要原因是:使用 Executors 建立線程池不會傳入核心參數,而是采用的預設值,這樣的話我們往往會忽略掉裡面參數的含義,如果業務場景要求比較苛刻的話,存在資源耗盡的風險;另外采用 ThreadPoolExecutor 的方式可以讓我們更加清楚地了解線程池的運作規則,不管是面試還是對技術成長都有莫大的好處。

改了變量,其他線程可以立即知道。保證可見性的方法有以下幾種:

volatile

加入 volatile 關鍵字的變量在進行彙編時會多出一個 lock 字首指令,這個字首指令相當于一個記憶體屏障,記憶體屏障可以保證記憶體操作的順序。當聲明為 volatile 的變量進行寫操作時,那麼這個變量需要将資料寫到主記憶體中。

由于處理器會實作緩存一緻性協定,是以寫到主記憶體後會導緻其他處理器的緩存無效,也就是線程工作記憶體無效,需要從主記憶體中重新重新整理資料。