天天看點

程式員的視角:java 線程(轉)

在我們開始談線程之前,不得不提下程序。

無論程序還是線程都是很抽象的概念,有一個關于程序和線程很形象的比喻能幫我們更好的了解。

程序就像個房子,房子是一個包含了特定屬性的容器,例如空間大小、卧室數量等。 如果你也這樣看的話,那麼房子自己不會主動做任何事情,它是被動的對象。 而線程則像是房中的居住者,它是主動的對象——居住者要使用不同的房間、看電視、煮飯、洗澡等等。 房子占據着一塊真實的土地,正像程序占據着記憶體。 而房子的居住者可以自由出入所有的房間,而程序中的線程也是類似的,可以自由通路任何程序占據的記憶體。

按照教科書上的定義,程序是資源管理的最小機關,線程是程式執行的最小機關。 通過上面的比喻,我們可以更容易的了解程序和線程的關系。 程序隻是一個容器對象,它負責占據資源(記憶體位址、檔案I/O),而線程共享程序的資源,作為CPU排程的基本機關可以被獨立排程。

線程實作

回到我們的題目:java 線程。 java 作為一個跨平台的語言,自然要提供一個跨平台的線程實作。 線程按類型可以分為核心線程(Kernel-Level Thread)和使用者線程(User Thread),分類的标準主要是線程的排程者在核内還是在核外。 早期時,一些作業系統因為沒有提供線程的原生實作,是以早在JDK1.2之前,java是基于使用者線程來實作的。 使用者線程是相對核心線程而言,核心線程自然是由作業系統核心支援的線程,由核心來管理和排程。 後來主流作業系統都支援了線程,是以現在java都采用原生線程來實作了。

既然現在的java線程都采用原生系統線程來實作,那麼是否每個java線程就對應一個系統核心線程? 對sun jdk而言,在Windows和Linux中都是采用的一對一模型,Linux提供一種稱為輕量級程序(LWP)的進階抽象來避免應用直接使用核心線程。 而在像Solaris這樣的系統中則不一定了,因為它支援多對多模型。 不過對于底層系統的線程模型到底如何,對java線程而言都是被屏蔽了的,jvm層面提供了一個統一的抽象線程模型。 下圖展示了在Linux上java線程實作的模型圖

程式員的視角:java 線程(轉)

線程數量

曾經碰到一個問題,java程式運作中抛出一個OOM錯誤如下:

​​

​java.lang.OutOfMemoryError: unable to create new native thread​

​這個問題的原因可能有兩種,一種是記憶體真的不足了,自然無法再建立線程。 另外一種其實是來自作業系統的限制,比如在Linux中,java線程會映射為輕量級程序,那麼建立線程的數量自然會受到系統程序數量等資源限制的限制。

對于一個java程序到底能建立多少線程呢,一般我們按經驗線程都是在幾十到幾百之間,頂多1、2k了。 這是為什麼呢?java有個啟動參數​

​-Xss1m​

​表明每個線程棧大小為1m,那麼對記憶體一般2G的話,總線程數達到2k感覺上都是不可能的。 但實際上做個實驗在循環中不斷建立新線程,可以不斷建立多達幾萬的線程,這又是為什麼? 原因是新建立的線程其實僅僅配置設定了記憶體位址空間,但并沒有實際去占用那1m的棧空間,棧空間是線上程使用時才去實際占用的。 是以經驗是對的,一般對2G的堆記憶體空間線程數量根據應用類型在幾十到幾百之間是合适的。

線程狀态

java定義了6種線程狀态,任一時刻一個線程處于其中一種狀态,其狀态轉換關系如下圖:

程式員的視角:java 線程(轉)
NEW
   新建立未啟動的線程處于該狀态
2. RUNNABLE
   調用了start()方法後,線程進入RUNNABLE狀态
3. WAITING
   不設定timeout的Object.wati()、Thread.join()等方法會讓線程進入無限等待,需要等待其他線程顯式的喚醒。
4. TIMED_WAITING
   Thead.sleep()或設定了timeout的Object.wati()、Thread.join()等方法讓線程進入限期等待。
5. BLOCKED
   阻塞狀态,線程在等待進入同步區域。
6. TERMINATED      

從上面的狀态圖可以看出,線程從建立、執行到結束是單向的,期間可能會經曆等待和阻塞狀态,線程執行結束進入終止狀态後将不能再重複使用。 任何時候一個CPU核隻能執行一個線程,也就是說同時并行運作的線程數與CPU核數相等。 在作業系統核心層面,線程隻有配置設定了CPU的執行時間片,才算處于​

​RUNNING​

​​狀态。 而當有大于CPU核數的線程需要執行,沒有配置設定到CPU執行時間片的線程則處于​

​READY​

​​狀态。 ​

​RUNNING​

​​和​

​READY​

​​都是線程在核心的狀态,同時映射到java的​

​RUNNABLE​

​​狀态。 ​

​RUNNABLE​

​正如其名,表示可運作的狀态,并非正在運作的狀态。

線程池

java程式設計不可避免的要使用線程,而使用線程更常見的方式是使用線程池。 說起池這個東西,我們應該比較熟悉,例如:連接配接池。 其實池就是一個容器,裡面有一堆預先建立好的對象,我們就稱其為對象池,而當這個對象具體為線程,那就是線程池了。 前面講線程狀态說過,線程執行從run()方法退出就會進入終止狀态,那麼這個線程就消亡了,不能再複用。 線程池的概念就是要複用線程,避免建立開銷,那麼如何複用呢,其實就是要讓池中的線程不用從run()方法中退出。 是以為了複用線程,池的實作會與一個阻塞隊列結合,空閑時線程阻塞在隊列上等待任務到來,任務執行結束後再重新阻塞,永遠不會退出。

jdk1.5引入了​

​java.util.concurrent​

​​并發包後,我們可以很友善的通過​

​ThreadPoolExecutor​

​來建立線程池

public ThreadPoolExecutor
(
    int corePoolSize,
    int maximumPoolSize,
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue,
    ThreadFactory threadFactory,
    RejectedExecutionHandler handler
)      

如上所示的構造方法中,​

​corePoolSize​

​​、​

​maximumPoolSize​

​​和​

​workQueue​

​​的關系一直讓人容易誤解。 當待執行任務數大于​

​corePoolSize​

​​時,多出的任務請求會被放進 ​

​workQueue​

​​中等待執行,直到​

​workQueue​

​​滿了後 才會繼續啟動新線程直到總線程數達到​

​maximumPoolSize​

​的大小,其示意圖如下。

程式員的視角:java 線程(轉)