JVM--內存模型與線程
一、硬件與效率的一致性
計算機的存儲設備與處理器的運算速度存在幾個數量級的差距,現在計算機系統不得不在內存和處理器之間增加一層高速緩存(cache)來作為緩沖。將運算需要的數據復制到緩存中,讓運算能夠快速進行,當運算結束的時候再講數據從緩存同步到內存中,這樣處理器無須等待緩慢的內存讀寫。除了增加高速緩存外,為了使處理器的內存的運算單元能被充分的利用,處理器可能對輸入的代碼進行亂序執行優化,即常說的重排序,計算後對亂序執行的結果重組,保證結果與順序執行代碼的結果一致,但是並不保證各個語句的計算順序與代碼順序一致。
基於高速緩存的存儲交互很好的解決了處理器與內存的速度矛盾,引進了一個新的問題:緩存一致性。在多處理器計算機系統中,每個處理器都有自己的高速緩存,而它們又共享同一主內存,當處理器的運算任務涉及到同一塊主內存區域的時候,很可能導致各自的緩存數據不一致。解決這個問題,需要各個處理器訪問緩存的時候遵循一些協議,在讀寫時根據協議來進行操作。
二、java內存模型
內存模型可以理解為在特定的操作協議下,對特定的內存或高速緩存進行讀寫訪問的過程抽象,不同架構的物理機器可以擁有不一樣的內存模型,而java虛擬機有自己的內存模型。同樣的,java虛擬機的即時編譯器中有類似的指令重排序優化功能。java虛擬機規範中定義的內存模型用來屏蔽掉各種硬件環境和操作系統的內存訪問差異,以實現讓java程序在各個平臺都能達到一致的內存訪問效果。在jdk1.5發布後,java的內存模型已經成熟和完善起來。
java內存模型的主要目標是為了定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量的底層細節。這裏的變量與java語言中說的變量不是同一個東西,包括:實例字段,靜態變量和構成數組對象的元素,但是不包括局部變量和方法參數(線程私有,不會共享,自然不存競爭問題)。java內存模型不限制執行引擎使用處理器的寄存器或緩存來和主內存進行交互,不限制即時編譯器進行重排序優化。java內存模型的規範如下:
1.主內存與工作內存
Java內存模型規定 1.所有的變量都存儲在主內存(虛擬機內存的一部分)。 2.每條線程有自己的工作內存,線程的工作內存保存了被該線程使用到的變量的主內存副本拷貝,線程對變量的讀取,賦值等必須在工作內存中進行。 3.不同線程之間無法直接訪問對方工作內存中的變量,線程間的變量值傳遞需要借助主內存完成。
2.java內存間的交互操作
內存交互:即一個變量如何從主內存拷貝到工作內存,又如何從工作內存同步到主內存之類的實現細節。Java內存模型定義八種操作來完成,並且下面八種操作都是原子的,不可再分的。如下:
lock(鎖定):作用於主內存中的變量,它把變量標識為一條線程獨占的狀態。
unlock(解鎖):作用主內存中的變量,它把一個處於鎖定狀態的變量釋放出來,釋放後的變量才可以被其他線程鎖定。
read(讀取):作用於主內存中的變量,把一個變量從主內存傳輸到線程的工作內存中,以便隨後的load操作。
load(載入):作用於工作內存中的變量,把read操作從主內存中得到的變量放入工作內存的變量副本中。
use(使用):作用於工作內存的變量,把一個工作內存中的變量的值傳遞給執行引擎,每當虛擬機遇到一個需要使用到變量的值得字節碼指令的時候將會執行這個操作。
assign(賦值):作用工作內存的變量,把從執行引擎接收到的值付賦給工作內存中的變量。
store(存儲):作用於工作內存中的變量,把工作內存中的一個變量的值傳遞到主內存中,以便後續的write操作。
write(寫入):作用於主內存中的變量,把store操作得到的變量的寫入到主內存中對應的變量中。
註意:java內存模型要求read和load操作順序執行,同時要求store和write操作順序執行。
3.volatile變量的特殊規則
- 保證volatile變量對所有線程的可見性,與普通變量的區別保證變量的新值能立即同步到主內存,以及每次使用的時候立即從主內存刷新。
- volatile變量禁止指令重排序優化(主要通過內存屏障指令來實現)
內存屏障:指重排序的時候,不能把後面的指令重排序到內存屏障之前的位置。
4.long和doubel型變量的特殊規則
允許虛擬機將沒被volatile修飾的64位數據的讀寫劃分為兩次32位的操作來進行。如long和double變量。但是目前商用虛擬機幾乎把64數據的讀寫操作作為原子操作來對待。
5.原子性、可見性和有序性
原子性:即一個操作或者多個操作 要麽全部執行並且執行的過程不會被任何因素打斷,要麽就都不執行。原子代表不可分割性。基本數據類型的訪問讀寫具備原子性。synchronized塊之間的操作具備原子性。
可見性:當一個線程修改共享變量的值,其他線程能夠立即知道這個修改。java中volatile,synchronized和final能夠實現變量的可見性。
有序性:線程內表現為串行語義,指令重排序和工作內存與主內存同步延遲的現象導致在其他線程觀察另外一個線程,其操作都是無序的。java提供volatile和synchronized兩個關鍵字來保證線程之間操作的有序性。
5.先行發生原則
java語言中有一個先行發生的原則,即happen-before,是判斷是否存在競爭和線程是否安全的主要依據。java內存模型規定天然先行發生關系:
- 程序次序規則:一個線程內,按照代碼順序,書寫在前面的操作先行發生於書寫在後面的操作;
- 鎖定規則:一個unLock操作先行發生於後面對同一個鎖額lock操作;
- volatile變量規則:對一個變量的寫操作先行發生於後面對這個變量的讀操作;
- 線程啟動規則:Thread對象的start()方法先行發生於此線程的每個一個動作;
- 線程中斷規則:對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生;
- 線程終結規則:線程中所有的操作都先行發生於線程的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執行;
- 對象終結規則:一個對象的初始化完成先行發生於他的finalize()方法的開始;
- 傳遞規則:如果操作A先行發生於操作B,而操作B又先行發生於操作C,則可以得出操作A先行發生於操作C;
三、java線程在虛擬機中的實現
線程是比進程更輕量級的調度執行單位,線程的引入,可以把一個進程的資源分配和調度分開,各個線程可以共享進程資源,又可以獨立調度(線程是CPU調度的基本單位)。主流的操作系統都提供了線程實現,Java語言則提供在不同硬件和操作系統平臺下對線程的統一處理。每個執行了start()且還未結束的java.lang.Thread類的實例就代表一個線程。
1.Java線程的實現
Java線程模型在jdk1.2後是基於操作系統原生線程模型來實現的,因此操作系統支持怎樣的線程模型,在很大程度上決定了Java虛擬機的線程是怎樣映射的。對於sun JDK來說,它在windows和linux都是使用一對一的線程模型實現,即一條java線程映射到一條輕量級進程之中。
2.Java線程調度
線程調度:系統為線程分配處理器使用權的過程。主要兩種方式:協同式線程調度和搶占式線程調度。java線程調度取決於操作系統的線程調度模式。
3.線程狀態轉換
java語言定義五種線程狀態,任意一個時間點,一個線程只能有且只有其中一種狀態,五種狀態如下:
1. 新建狀態(New) : 線程對象被創建後,就進入了新建狀態。例如,Thread thread = new Thread()。 2. 就緒狀態(Runnable): 也被稱為“可執行狀態”。線程對象被創建後,其它線程調用了該對象的start()方法,從而來啟動該線程。例如,thread.start()。處於就緒狀態的線程,隨時可能被CPU調度執行。 3. 運行狀態(Running) : 線程獲取CPU權限進行執行。需要註意的是,線程只能從就緒狀態進入到運行狀態。 4. 阻塞狀態(Blocked) : 阻塞狀態是線程因為某種原因放棄CPU使用權,暫時停止運行。直到線程進入就緒狀態,才有機會轉到運行狀態。阻塞的情況分三種: (01) 等待阻塞 -- 通過調用線程的wait()方法,讓線程等待某工作的完成。 (02) 同步阻塞 -- 線程在獲取synchronized同步鎖失敗(因為鎖被其它線程所占用),它會進入同步阻塞狀態。 (03) 其他阻塞 -- 通過調用線程的sleep()或join()或發出了I/O請求時,線程會進入到阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。
5. 結束狀態(Terminated): 線程執行完了或者因異常退出了run()方法,該線程結束生命周期。
線程狀態轉換關系如下:
JVM--內存模型與線程