標籤:thread 效能 版本 模式 sel oop 就會 java _id
並發不一定要依賴多線程(如PHP中很常見的多進程並發),但是在Java裡面談論並發,大多數都與線程脫不開關係。
線程是比進程更輕量級的調度執行單位,線程的引入,可以把一個進程的資源分派和執行調度分開,各個線程既可以共用進程資源(記憶體位址、檔案I/O等),又可以獨立調度(線程是CPU調度的基本單位)。
主流的作業系統都提供了線程實現,Java語言則提供了在不同硬體和作業系統平台下對線程操作的統一處理,每個已經執行start()且還未結束的java.lang.Thread類的執行個體就代表了一個線程。我們注意到Thread類與大部分的Java API有顯著的差別,它的所有關鍵方法都是聲明為Native的。在Java API中,一個Native方法往往意味著這個方法沒有使用或無法使用平台無關的手段來實現(當然也可能是為了執行效率而使用Native方法,不過,通常最高效率的手段也就是平台相關的手段)。
線程的實現
實現線程主要有3種方式:使用核心線程實現、使用使用者線程實現和使用使用者線程加輕量級進程混合實現。
使用核心線程實現
核心線程(Kernel-Level Thread,KLT)就是直接由作業系統核心(Kernel,下稱核心)支援的線程,這種線程由核心來完成線程切換,核心通過操縱調度器(Scheduler)對線程進行調度,並負責將線程的任務映射到各個處理器上。每個核心線程可以視為核心的一個分身,這樣作業系統就有能力同時處理多件事情,支援多線程的核心就叫做多線程核心(Multi-Threads Kernel)。
程式一般不會直接去使用核心線程,而是去使用核心線程的一種進階介面——輕量級進程(Light Weight Process,LWP),輕量級進程就是我們通常意義上所講的線程,由於每個輕量級進程都由一個核心線程支援,因此只有先支援核心線程,才能有輕量級進程。這種輕量級進程與核心線程之間1:1的關係稱為一對一的執行緒模式。
由於核心線程的支援,每個輕量級進程都成為一個獨立的調度單元,即使有一個輕量級進程在系統調用中阻塞了,也不會影響整個進程繼續工作,但是輕量級進程具有它的局限性:
首先,由於是基於核心線程實現的,所以各種線程操作,如建立、析構及同步,都需要進行系統調用。而系統調用的代價相對較高,需要在使用者態(User Mode)和核心態(Kernel Mode)中來回切換。
其次,每個輕量級進程都需要有一個核心線程的支援,因此輕量級進程要消耗一定的核心資源(如核心線程的棧空間),因此一個系統支援輕量級進程的數量是有限的。
使用使用者線程實現
從廣義上來講,一個線程只要不是核心線程,就可以認為是使用者線程(User Thread,UT),因此,從這個定義上來講,輕量級進程也屬於使用者線程,但輕量級進程的實現始終是建立在核心之上的,許多操作都要進行系統調用,效率會受到限制。
而狹義上的使用者線程指的是完全建立在使用者空間的線程庫上,系統核心不能感知線程存在的實現。使用者線程的建立、同步、銷毀和調度完全在使用者態中完成,不需要核心的協助。如果程式實現得當,這種線程不需要切換到核心態,因此操作可以是非常快速且低消耗的,也可以支援規模更大的線程數量,部分高效能資料庫中的多線程就是由使用者線程實現的。這種進程與使用者線程之間1:N的關係稱為一對多的執行緒模式。
使用使用者線程的優勢在於不需要系統核心支援,劣勢也在於沒有系統核心的支援,所有的線程操作都需要使用者程式自己處理。線程的建立、切換和調度都是需要考慮的問題,而且由於作業系統只把處理器資源分派到進程,那諸如“阻塞如何處理”、“多處理器系統中如何將線程映射到其他處理器上”這類問題解決起來將會異常困難,甚至不可能完成。因而使用使用者線程實現的程式一般都比較複雜,此處所講的“複雜”與“程式自己完成線程操作”,並不限制程式中必須編寫了複雜的實現使用者線程的代碼,使用使用者線程的程式,很多都依賴特定的線程庫來完成基本的線程操作,這些複雜性都封裝線上程庫之中,除了以前在不支援多線程的作業系統中(如DOS)的多線程程式與少數有特殊需求的程式外,現在使用使用者線程的程式越來越少了,Java、Ruby等語言都曾經使用過使用者線程,最終又都放棄使用它。
使用使用者線程加輕量級進程混合實現
線程除了依賴核心線程實現和完全由使用者程式自己實現之外,還有一種將核心線程與使用者線程一起使用的實現方式。在這種混合實現下,既存在使用者線程,也存在輕量級進程。使用者線程還是完全建立在使用者空間中,因此使用者線程的建立、切換、析構等操作依然廉價,並且可以支援大規模的使用者線程並發。而作業系統提供支援的輕量級進程則作為使用者線程和核心線程之間的橋樑,這樣可以使用核心提供的線程調度功能及處理器映射,並且使用者線程的系統調用要通過輕量級線程來完成,大大降低了整個進程被完全阻塞的風險。在這種混合模式中,使用者線程與輕量級進程的數量比是不定的,即為N:M的關係。許多UNIX系列的作業系統,如Solaris、HP-UX等都提供了N:M的執行緒模式實現。
Java線程的實現
對於Sun JDK來說,它的Windows版與Linux版都是使用一對一的執行緒模式實現的,一條Java線程就映射到一條輕量級進程之中,因為Windows和Linux系統提供的執行緒模式就是一對一的。
在Solaris平台中,由於作業系統的線程特性可以同時支援一對一(通過Bound Threads或Alternate Libthread實現)及多對多(通過LWP/Thread Based Synchronization實現)的執行緒模式,因此在Solaris版的JDK中也對應提供了兩個平台專有的虛擬機器參數:-XX:+UseLWPSynchronization(預設值)和-XX:+UseBoundThreads來明確指定虛擬機器使用哪種執行緒模式。
Java線程調度
線程調度是指系統為線程分配處理器使用權的過程,主要調度方式有兩種,分別是協同式線程調度(Cooperative Threads-Scheduling)和搶佔式線程調度(Preemptive Threads-Scheduling)。
協同式調度
如果使用協同式調度的多線程系統,線程的執行時間由線程本身來控制,線程把自己的工作執行完了之後,要主動通知系統切換到另外一個線程上。協同式多線程的最大好處是實現簡單,而且由於線程要把自己的事情幹完後才會進行線程切換,切換操作對線程自己是可知的,所以沒有什麼線程同步的問題。Lua語言中的“協同常式”就是這類實現。它的壞處也很明顯:線程執行時間不可控制,甚至如果一個線程編寫有問題,一直不告知系統進行線程切換,那麼程式就會一直阻塞在那裡。很久以前的Windows 3.x系統就是使用協同式來實現多進程多任務的,相當不穩定,一個進程堅持不讓出CPU執行時間就可能會導致整個系統崩潰。
搶佔式調度
如果使用搶佔式調度的多線程系統,那麼每個線程將由系統來分配執行時間,線程的切換不由線程本身來決定(在Java中,Thread.yield()可以讓出執行時間,但是要擷取執行時間的話,線程本身是沒有什麼辦法的)。在這種實現線程調度的方式下,線程的執行時間是系統可控的,也不會有一個線程導致整個進程阻塞的問題,Java使用的線程調度方式就是搶佔式調度。在JDK後續版本中有可能會提供協程(Coroutines)方式來進行多任務處理。與前面所說的Windows 3.x的例子相對,在Windows 9x/NT核心中就是使用搶佔式來實現多進程的,當一個進程出了問題,我們還可以使用工作管理員把這個進程“殺掉”,而不至於導致系統崩潰。
線程優先順序
雖然Java線程調度是系統自動完成的,但是我們還是可以“建議”系統給某些線程多分配一點執行時間,另外的一些線程則可以少分配一點——這項操作可以通過設定線程優先順序來完成。Java語言一共設定了10個層級的線程優先順序(Thread.MIN_PRIORITY至Thread.MAX_PRIORITY),在兩個線程同時處於Ready狀態時,優先順序越高的線程越容易被系統選擇執行。不過,線程優先順序並不是太靠譜,原因是Java的線程是通過映射到系統的原生線程上來實現的,所以線程調度最終還是取決於作業系統,雖然現在很多作業系統都提供線程優先順序的概念,但是並不見得能與Java線程的優先順序一一對應,如Solaris中有2147483648(232)種優先順序,但Windows中就只有7種,比Java線程優先順序多的系統還好說,中間留下一點空位就可以了,但比Java線程優先順序少的系統,就不得不出現幾個優先順序相同的情況了,表12-1顯示了Java線程優先順序與Windows線程優先順序之間的對應關係,Windows平台的JDK中使用了除THREAD_PRIORITY_IDLE之外的其餘6種線程優先順序。
上文說到“線程優先順序並不是太靠譜”,不僅僅是說在一些平台上不同的優先順序實際會變得相同這一點,還有其他情況讓我們不能太依賴優先順序:優先順序可能會被系統自行改變。例如,在Windows系統中存在一個稱為“優先順序推進器”(Priority Boosting,當然它可以被關閉掉)的功能,它的大致作用就是當系統發現一個線程執行得特別“勤奮努力”的話,可能會越過線程優先順序去為它分配執行時間。因此,我們不能在程式中通過優先順序來完全準確地判斷一組狀態都為Ready的線程將會先執行哪一個。
線程狀態轉換
Java語言定義了5種線程狀態,在任意一個時間點,一個線程只能有且只有其中的一種狀態,這5種狀態分別如下。
- 建立(New):建立後尚未啟動的線程處於這種狀態。
- 運行(Runable):Runable包括了作業系統線程狀態中的Running和Ready,也就是處於此狀態的線程有可能正在執行,也有可能正在等待著CPU為它分配執行時間。
- 無限期等待(Waiting):處於這種狀態的線程不會被分配CPU執行時間,它們要等待被其他線程顯式地喚醒。以下方法會讓線程陷入無限期的等待狀態:
-
- 沒有設定Timeout參數的Object.wait()方法。
- 沒有設定Timeout參數的Thread.join()方法。
- LockSupport.park()方法。
4.限期等待(Timed Waiting):處於這種狀態的線程也不會被分配CPU執行時間,不過無須等待被其他線程顯式地喚醒,在一定時間之後它們會由系統自動喚 醒。以下方法會讓線程進入限期等待狀態:
-
- Thread.sleep()方法。
- 設定了Timeout參數的Object.wait()方法。
- 設定了Timeout參數的Thread.join()方法。
- LockSupport.parkNanos()方法。
- LockSupport.parkUntil()方法。
5.阻塞(Blocked):線程被阻塞了,“阻塞狀態”與“等待狀態”的區別是:“阻塞狀態”在等待著擷取到一個獨佔鎖定,這個事件將在另外一個線程放棄這個鎖的時候 發生;而“等待狀態”則是在等待一段時間,或者喚醒動作的發生。在程式等待進入同步地區的時候,線程將進入這種狀態。
6.結束(Terminated):已終止線程的線程狀態,線程已經結束執行。
Java的執行緒模式