1. 程式人生 > >多線程-Java內存模型與線程

多線程-Java內存模型與線程

深入理解 接收 沒有 對象 編程 性問題 訪問 作用 jdk1.5

概述

為了解決內存與cpu之間的速度矛盾,在兩者之前引入了寫速度盡可能接近cpu運算速度的高速緩存:將運算需要使用到的數據復制到緩存中,讓運算能快速進行,當運算結束後再從緩存同步回內存之中,這樣處理就無須等待緩慢的內存讀寫了。

但是這也為計算機系統帶來更高的復雜度,因為它引入了一個新的問題:緩存一致性。在多處理系統中,每個處理都有自己的高速緩存,而它們又共享同一主內存,當多個處理器的運算任務都涉及同一塊主內存區域時,將可能導致各自的緩存數據不一致。

技術分享

內存模型:可以理解為在特定的操作協議下,對特定的內存和高速緩存進行讀寫訪問的過程抽象。不同架構的物理機器可以擁有不一樣的內存模型,而Java虛擬機也有自己的內存模型。

Java內存模型(Java Memory Model,JMM)從jdk1.2之後建立起來並在jdk1.5中完備過的內存模型。Java虛擬機規範中試圖定義一種Java內存模型來屏蔽掉各種硬件和操作系統的內存訪問差異,以實現讓Java程序在各種平臺都能達到一致的內存訪問效果。C、C++等直接使用武力硬件和操作系統的內存模型,會由於不同平臺內存模型的差異,導致程序無法完美的移植。

主內存和工作內存

Java內存模型的主要目標是定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣底層細節。 這裏的比那裏與Java編程中所說的變量有所區別,它包括了實例字段,靜態字段和構成數組對象的元素,但不包括局部變量和方法參數(線程私有的,不會被共享,不存在競爭)

Java內存模型規定了所有的變量都存儲在主內存(Main Memory)中。每條線程還有自己的工作內存(Working Memory),工作內存中保存了被該線程使用的變量的主內存副本拷貝,線程對變量的所有操作(讀取,賦值等)都必須在工作內存中進行,而不能直接讀寫主內存中的變量。不同的線程之間也無法直接訪問對方工作內存中的變量,線程見變量值的傳遞均需要通過主內存來完成。

技術分享

這裏的主內存和工作內存與Java內存區域中的Java堆,棧,方法區等不適同一個層次的內存劃分。

主內存和工作內存之間具體的交互協議:一個變量如何從主內存拷貝到工作內存,如果從工作內存同步回主內存等的實現細節。

Java內存模型定了8中操作來完成,虛擬機必須保證每一種操作都是原子的,不可再分的:

(1)lock:鎖定,作用於主內存的變量,它把一個變量標識為一條線程獨占的狀態;

(2)unlock:解鎖,作用於主內存的變量,它把一個處於鎖定狀態的變量釋放出來,釋放後的變量才可以被其他線程鎖定;

(3)read:讀取,作用於主內存的變量,它把一個變量的值從主內存傳輸到線程的工作內存中,以便隨後的load動作使用;

(4)load:載入,作用於工作內存的變量,它把read操作從內存中得到的變量值放入工作內存的變量副本中;

(5)use:使用,作用於工作內存中的變量,它把工作內存中一個變量的值傳遞給執行引擎,每當虛擬機遇到一個需要使用到變量的值得字節碼指令時將會執行這個操作;

(6)assign:賦值,作用於工作內存的變量,它把一個從執行引擎接收到的值賦給工作內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操作;

(7)store:存儲,作用於工作內存的變量,它把工作內存中一個變量的值傳送到主內存中,以便隨後的write操作使用;

(8)write:寫入,作用於主內存的變量,它把store操作從工作內存中得到的變量的值放入主內存的變量中。

如果要把一個變量從主內存復制到工作內存,那就要順序執行read和load操作,如果把變量從工作內存同步回主內存,就要順序地執行store和write操作。

Java內存模型還固定了在執行8種基本操作時必須滿足的規則:

(1)不允許read和load,store和write操作之一單獨出現;

(2)不允許一個線程丟棄它的最近的assign操作,即變量在工作內存中改變之後必須把該變化同步回主內存;

(3)不允許一個線程無原因的(沒有發生過任何assign操作)把數據從工作內存同步回主內存中;

(4)一個新變量只能在主內存中“誕生”;

(5)一個變量在同一個時刻只允許一條線程對其進行lock操作,並且可以被同一條線程重復執行多次;

(6)多一個變量執行lock操作,將會清空工作內存中此變量的值,在執行引擎使用這個變量前,需要重新執行load或assign操作初始化變量的值;

(7)如果一個變量事先沒有被lock鎖定,就不允許對他執行unlock,也不允許去unlock一個被其他線程鎖定的變量;

(8)對一個變量執行unlock操作之前,必須先把此變量同步回主內存中

volatile

volatile具備兩種特性:(1)保證此變量對所有線程可見性(2)禁止指令重排序優化。

volatile變量在規格線程中的工作內存中不存在一致性的問題:在規格線程的工作內存中,volatile變量也可以存在不一致的情況,但由於每次使用之前都要先刷新,執行引擎看不到不一致的情況,因此可以認為不存在不一致性問題。但是Java裏面的運算並非原子操作,導致volatile變量的運算在並發下一樣是不安全的。

原子性,可見性與有序性

Java內存模型是圍繞著在並發過程中如何處理原子性,可見性和有序性這3個特征來建立的

(1)原子性:Java內存模型來直接保證的原子性變量操作包括read,load,assign,use,store,write;

lock和unlock操作對應的字節碼指令是monitorenter和monitorexit,反應到Java代碼中就是同步塊--synchronized關鍵字,因此在synchronized塊之間的操作也具備原子性。

(2)可見性:當一個線程修改了共享變量的值,其他線程能夠立即得知這個修改。Java內存模型是通過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值這種依賴主內存作為傳遞媒介的方式來實現可見性的,無論普通變量還是volatile變量都是如此,普通變量與volatile變量的區別是:volatile的特殊規則保證了新值能立即同步到主內存,以及每次使用前立即從主內存刷新。因此,可以說volatile保證了多線程操作時變量的可見性,而普通變量則不能保證這一點。

(3)有序性:Java語言提供了volatile和synchronized兩個關鍵字來保證線程之間操作的有序性。

先行發生原則

(1)程序次序規則

(2)管程鎖定規則

(3)volatile變量規則

(4)線程啟動規則

(5)線程終止規則

(6)線程中斷規則

(7)對象終結規則

(8)傳遞性

一個操作“時間上的先發生”不代表這個操作會是“先行發生”,一個操作是“先行發生”也未必是“時間上的先發生”。

衡量並發安全問題的時候不要受到時間順序的幹擾,一切必須以先行發生原則為準。

線程的實現

主要有3種方式:(1)使用內核線程實現(2)使用用戶線程實現(3)使用用戶線程加輕量級進程混合實現

(1)內核線程:就是直接由操作系統(kernel,內核)支持的線程,這種線程由內核來完成線程切換,內核通過操縱調度器對線程進行調度,並負責將線程的任務映射到規格處理器上。一般不會直接去使用內核線程,而是使用內核線程的一種高級接口--輕量級進程。

(2)用戶線程:廣義上,一個線程只要不是內核線程,就可以認為是用戶線程,從這個定義上來說,輕量級進程也數據用戶線程。狹義上,用戶線程是指完全建立在用戶控件的線程庫上,系統內核不能感知線程存在的實現。

(3)用戶線程加輕量級進程混合:即前兩種的混合。在這種混合實現下,即存在用戶線程,也存在輕量級進程,用戶線程還是完全建立在用戶空間中,因此用戶線程的創建,切換,析構等操作依然廉價,並且可以支持大規模的用戶線程並發。而操作系統提供支持的輕量級進程則作為用戶線程和內核線程之間的橋梁,這樣可以使用內核提供的線程調度功能及處理器映射,並且用戶線程的系統調用要通過輕量級進程來完成,大大降低了整個進程被完全阻塞的風險。

Java線程實現

(1)實現:從jdk1.2之後,線程模型替換為基於操作系統原生線程模型來實現的。操作系統支持怎麽樣的線程模型,在很大程度上決定了Java虛擬機線程是怎樣映射的,這點在不同的平臺上沒有辦法達成一致,虛擬機規範中並未限定Java線程需要使用哪種線程模型來實現。線程模型只對線程的並發規模和操作成本產生影響,對Java程序的編碼和運行過程來說,這些差異都是透明的。

Sun JDK它的Windows和Linux版本都是使用一對一的線程模型實現的,一條Java線程就映射到一條輕量級進程之中。

(2)指系統為線程分配處理器使用權的過程,主要調度方式有兩種:(A)協同式線程調度(B)搶占式線程調度;Java使用的調度方式就是搶占式調度。

(3)裝填轉換

  1. 新建:創建後尚未啟動的線程
  2. 運行:處於此狀態的線程有可能正在執行,也有可能正在等待著cpu為它分配執行時間
  3. 無限等待:不會被分配cpu執行時間,要等待被其他線程顯式地喚醒,如Object.wait(),Thread.join(),LockSupport.park()
  4. 有限等待:超時可由系統自動喚醒。如Thread.sleep(),Object.wait(long),Thread.join(long),LockSupport.parkNanos(),LockSupport.parkUntil()
  5. 阻塞:阻塞狀態是指在等待著獲取到一個排他鎖;等待狀態是指在等待一段時間或喚醒動作的發生。
  6. 結束:已終止。

線程安全

定義:當多個線程訪問一個對象時,如果不用考慮這些線程在運行時環境下的調度和交替執行,也不需要進行額外的同步,或者在調用方進行任何其他的協調操作,調用整個對象的行為都可以獲得正確的結果,那整個對象是線程安全的。

線程安全的實現方法

參考資料

深入理解Java虛擬機:JVM高級特性與最佳實踐(第二版)

多線程-Java內存模型與線程