先行先發生原則(happen-before原則)
先行先發生是指Java內存模型中定義的兩項操作之間的偏序關係。
如果說A先行於B,其實就是說在發生B操作之前,操作A產生的影響能被操作B觀察到,至於這個影響可以是修改內存中的共享變量也可以是發送消息、調用某個方法等。
happen-before要求前一個操作的執行結果對後一個操作可見,並且前一個操作按照順序排在第二個操作之前。
happen-before規則:
- **程序次序規則:**在一個線程內,按照程序代碼順序,書寫在前面的操作要先行發生於書寫在後面的操作。準確的說,應該是控制流順序而不是程序代碼的順序。
- 管程鎖定規則:一個unlock操作要先行發生於lock。這裏需要強調的是通一把鎖。
- volatile變量規則:對一個volatile變量的寫操作線性發生於後面對這個變量的讀操作,後面是指時間的先後順序。
- 線程啓動規則:Thread對象的start方法先行發生於此線程的每個動作。
- 線程終止規則:線程中的所有操作都先行發生於對此線程的終止檢測,比如線程A中執行
ThreadB.join()
;那麼線程B中的任意操作先行於A從ThreadB.join()
操作成功返回。 - 線程中斷規則:對線程
Thread.interrupt()
方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生。 - 對象終結規則:一個對象的初始化完成要先行於他的
finalize()
方法的開始。 - 傳遞性:如果操作A先行發生於操作B,操作B先行發生於操作C,那就可以得出操作A先行發生於C。
時間先後順序與happen-before原則之間基本沒有太大的關係,所以我們衡量併發安全問題的時候不要受到時間順序的干擾,一切以happen-before原則爲準。
實現線程的三種方式
線程是比進程更輕量的調度執行單位,線程的引入是把進程的資源分配和調度分開,進程是資源分配的基本單位,線程以後成爲了執行調度的基本單位。
實現線程的的方式主要有三種,分別是內核線程實現,用戶線程實現,用戶線程和輕量級進程組合實現
。
使用內核線程實現
內核線程是由操作系統內核支持的線程,內核通過調度器來調度內核線程,並負責將線程的任務映射到各個處理器上。每個內核線程可以視爲內核的一個分身。支持多線程的內核叫做多線程內核。
程序不會直接使用內核線程而是通過接口–輕量級進程來調用。每個輕量級進程都對應一個內核線程,所以他們之間的比例是1:1的關係。
使用輕量級進程實現線程的優點:因爲有內核線程的支持,每個輕量級進程都會成爲一個獨立的調度單位,即使其中有一個輕量級進程在系統調用過程中阻塞了,也不會影響整個進程繼續工作。
使用輕量級進程實現線程的缺點:輕量級進程需要很多次的系統調用,系統調用的代價是很高的,操作系統需要不停的在用戶態和 內核態之間進行切換。每個輕量級進程都有一個內核線程的支持,因此輕量級線程耗費的不少內核資源,所以說內核中的輕量級進程的數量是有限的。
使用用戶線程實現
從廣義上講,線程不是內核線程就是用戶線程,其實要這麼來說,輕量級進程也是用戶線程。
從狹義上講。用戶線程是指完全建立在用戶空間的線程庫上,系統內核不會感知到用戶線程的存在,其中用戶線程的同步,創建,銷燬都在用戶態下進行,不需要內核的參與。因此這種實現是非常快速並且是低消耗的。所以部分高性能的數據庫中的多線程就是由多用戶線程實現的。這種實現方式下的進程和線程之比是 1:N。也稱爲一對多線程模型。
這種實現方式的優點:不需要內核的幫助,快速高效且低消耗。
缺點:由於沒有內核參與所以考慮的問題是非常多的,導致實現過程過於複雜。
用戶線程和輕量級進程混合實現
線程除了以上兩種實現方式外,還有用戶線程和輕量級進程的混合使用。在這種模式下,既存在用戶線程也存在輕量級進程。操作系統提供輕量級進程作爲用戶線程和內核線程之間的橋樑。這樣可以使用內核提供的線程調度功能以及處理器映射,並且用戶線程的系統調用通過輕量級進程來完成,大大獎勵了整個進程被完全阻塞的風險,在這種模式下,用戶線程和輕量級進程的數量都是不確定的,所以是N:M 的關係。Unix系列的操作系統就提供了這種實現模式。
Java線程的實現
在JDK1.2之前是基於稱爲“綠色線程”的用戶線程來實現的。在JDK1.2中,線程模型改爲基於操作系統的線程模型,換句話說,操作系統選擇哪種線程模型,Java虛擬機就選擇哪種線程映射方式。線程模型只對線程的併發規模和操作成本產生影響,對Java的運行過程來說這些差異是感受不到的。
在Windows和Linux操作系統上,兩個操作系統都選擇的是1:1線程模型,一條Java線程就映射到一條輕量級進程中,因爲Windows和Linux系統提供的線程模型就是一對一的。
Java線程的調度
線程調度就是系統爲線程分配處理器使用權的過程,主要分爲兩種,一種是協同式,一種是搶佔式調度。
如果使用系協同式調度,那就是線程把自己的任務執行完成後才切換到另一個線程,線程佔用處理器的時間由線程自身決定。協同式的好處就是實現簡單,切換線程對線程本身來講是可知的,所以沒有什麼線程同步問題。
它的缺點也很明顯,線程執行時間不受限制,如果一個線程編寫的有問題,那他將一直佔據處理器的使用,程序一直阻塞在那兒,相當不穩定。
搶佔式調度中的每個線程的執行時間交由系統來決定,線程之間的切換不是由線程自身決定。在這種實現線程調度的方式下,線程的執行時間是系統可控的,也不會有一個線程阻塞導致整個進程阻塞的問題,而Java就是採用的搶佔式線程調度。
我們可以“建議”系統給某些線程分配多一點執行時間,給一些線程分配少一些執行時間,整個可以通過設置線程優先級來實現。Java一共設置的有10個線程優先級,在兩個線程同時處於就緒態的時候,優先級越高的線程越容易被系統選中執行。不過有時候這種採用優先級的方式又不太靠譜,因爲Java的線程最後是映射到系統的原聲線程來實現的,所以線程調度最終取決於操作系統。如果操作系統中的優先級設置的種類比Java設置的優先級的種類少,那麼就會出現幾個優先級相同的情況了。例如Windows設置的線程優先級就只有7種,Java設置的有10種,所以Windows至少得重複3種纔可以和Java進行對應。
Java線程狀態之間的轉換
具體看我這篇博文
參考:《深入理解Java虛擬機 第二版》周志明