併發處理的普遍應用是使得amdahl定律代替摩爾定律成爲計算機性能發展源動力的根本緣由,是人類壓榨計算機運算能力的最有力武器。java
上一篇《java 多線程—線程怎麼來的 》中咱們瞭解了線程在操做系統中的是如何派生出來的,這一篇咱們聊聊jvm的內存模型,瞭解一些jvm在內存操做中如何保證一致性問題的。c++
本篇主要包含如下內容:緩存
硬件的內存模型安全
jvm的內存模型多線程
happens-before架構
物理機併發處理的方案對於jvm的內存模型實現,也有很大的參考做用,畢竟jvm也是在硬件層上來作事情,底層架構也決定了上層的建築建模方式。併發
計算機併發並不是只是多個處理器都參與進來計算就能夠了,會牽扯到一些列硬件的問題,最直接的就是要和內存作交互。但計算機的存儲設備與處理器的預算速度相差太大,徹底不能知足處理器的處理速度,怎麼辦,這就是後續加入的一層讀寫速度接近處理器運算速度的高速緩存來做爲處理器和內存之間的緩衝。app
高速緩存一邊把使用的數據,從內存複製搬入,方便處理器快速運算,一邊把運算後的數據,再同步到主內存中,如此處理器就無需等待了。jvm
高速緩存雖然解決了處理器和內存的矛盾,但也爲計算機帶來了另外一個問題:緩存一致性。特別是當多個處理器都涉及到同一塊主內存區域的時候,將可能會致使各自的緩存數據不一致。性能
那麼出現不一致狀況的時候,以誰的爲準?
爲了解決這個問題,處理器和內存之間的讀寫的時候須要遵循必定的協議來操做,這類協議有:MSI、MESI、MOSI、Synapse、Firefly 以及 Dragon Protocol等。這就是上圖中處理器、高速緩存、以及內存之間的處理方式。
另外除了高速緩存以外,爲了充分利用處理器,處理器還會把輸入的指令碼進行亂序執行優化,只要保證輸出一致,輸入的信息能夠亂序執行重組,因此程序中的語句計算順序和輸入代碼的順序並不是一致。
上面咱們瞭解了硬件的內存模型,以此爲借鑑,咱們看看jvm的內存模型。
jvm定義的一套java內存模型爲了可以跨平臺達到一致的內存訪問效果,從而屏蔽掉了各類硬件和操做系統的內存訪問差別。這點和c和c++並不同,C和C++會直接使用物理硬件和操做系統的內存模型來處理,因此在各個平臺上會有差別,這一點java不會。
java的內存模型規定了全部的變量都存儲在主內存中,每一個線程擁有本身的工做內存,工做內存保存了該線程使用到的變量的主內存拷貝,線程對變量全部操做,讀取,賦值,都必須在工做內存中進行,不能直接寫主內存變量,線程間變量值的傳遞均須要主內存來完成。
一個變量如何從主內存拷貝到工做內存,而後發生改變又從工做內存同步到主內存的,jvm定義了8中操做來完成,並保證每一種操做都是原子的。咱們來看看有那些操做。
上圖就是一個變量從主內存到工做內存,通過使用和賦值以後,又同步到主內存之中。
讀取:
一、read:讀取主內存的變量,傳送到工做內存中。
二、load: 把剛讀取的變量,放入到工做內存的變量副本中。
修改:
三、use:把工做內存變量的值傳遞給執行引擎
四、assign: 把執行引擎收到的值賦值給工做內存的變量
寫入:
五、store:把工做內存的變量傳送會主內存中
六、write:把剛store的變量放入到主內存中
鎖定:
除了以上三種分類,還有鎖定操做,用來處理線程獨佔狀態。
lock:把主內存的一個變量鎖定。
unlock: 把主內存內,lock的變量釋放解鎖,釋放後能夠被其餘線程訪問。
若是把變量從主變量複製到工做內存中,就要順序的執行read和load操做,若是要把變量從工做內存同步回主內存,就要順序的執行strore和write操做,不容許read和load、store和write操做之一單獨出現,也不容許一個線程丟棄assign操做,也就是改變後必須同步到主內存中。
另外還有有些其餘的規則,好比變量不容許在工做線程中誕生,只能夠在主內存中誕生,因此方法內的局部變量也是在主內存中初始化的,並不是在工做線程中誕生。如此的多的規則,要記住不容易,下面講到的happen-before會將這些規則整合一塊兒,相信看完happen-before以後,會加深理解。
變量從誕生到賦值再回寫,這麼簡單的一個過程要分解爲8個操做,目的是爲了讓內存在高速讀取的同時,也能保持數據的一致性。
jvm內存模型是圍繞着併發過程當中如何處理原子性、可見性和有序性來創建的,咱們看下這三個特徵。
原子性:
jvm內存模型中直接保證的原子變量操做包括read、load、assign、use、store、write,基本數據的訪問讀寫都是具有原子性的。這就解釋了爲什麼多線程下對變量賦值操做不是安全的,由於一個賦值會包含5個操做。
若是要保持原子性,jvm提供了lock和unlock,這個直接在代碼層就是synchronized關鍵字,擁有synchronized關鍵字的同步塊,在對數據操做的時候,線程會先對變量lock,等操做完了在unlock,如此也具有原子操做了。
可見性:
可見性是指當一個線程修改了共享變量的值,其餘線程能夠當即得知 。好比輕同步關鍵字volatile就能夠保證這點,這個下次單獨講。除了volatile,synchronized和final關鍵字定義的變量,也實現了可見性。其餘的就沒辦法保證了,目前的可共享內存的多處理器架構上,一個線程沒法立刻(甚至永遠)看到另外一個線程操做產生的結果的。
有序性:
前面有講過,處理器爲了加快處理速度,會把執行順序打亂,只保證結果一致,而不保證順序一致。這就是指令的重排序。
具體的編譯器實現能夠產生任意它喜歡的代碼 -- 只要全部執行這些代碼產生的結果,可以和內存模型預測的結果保持一致。這爲編譯器實現者提供了很大的自由,包括操做的重排序。
jvm對定義了volatile和synchronize的關鍵變量,能夠保證操做的有序性,好比禁止指令重排序,保證線程之間的操做有序,讓一個變量在同一時刻只能有個線程對其lock操做。
前面鋪墊了這麼多的基礎知識,一直沒有講到jvm到底是如何併發期間,保證對定義有synchronize和volatile變量的一致性?
happen-before,是判斷數據是否存在競爭、線程是否安全的主要依據,依靠這個原則,咱們能夠一攬子解決併發環境下兩個操做是否可能存在衝突的全部問題。
咱們先看規則:
- 程序次序法則:若是在程序中,全部動做 A 出如今動做 B 以前,則線程中的每動做 A 都 happens-before 於該線程中的每個動做 B。
- 監視器鎖法則:對一個監視器的解鎖 happens-before 於每一個後續對同一監視器的加鎖。
- Volatile 變量法則:對 Volatile 域的寫入操做 happens-before 於每一個後續對同一 Volatile 的讀操做。
- 傳遞性:若是 A happens-before 於 B,且 B happens-before C,則 A happens-before C。
- 線程啓動規則: thread對象的start()方法線性發生與此線程每一個動做。
- 線程終止規則:線程中的全部操做都線性發生對此線程的終止檢測。
- 線程中斷規則:對線程interrupt()方法的調用先行發生與被中斷線程的代碼檢測。
- 對象終結規則:一個對象的初始化完成線性發生於finalize()。
目前咱們只關注前4個就能夠了,後續的用到了再聊。volatile咱們此次仍是先不聊,咱們總結一下一、二、4,很明顯是講一個對一個同步塊內的邏輯進行串行化操做,看下示例
int count = 0; public synchronized void increCount() { count++; }
上例子中咱們對increCount方法進行了同步處理,那麼好比咱們有線程A、B、C同時調用這個方法會怎樣處理?
從上圖中咱們能夠看到,對於increCount來講,多個線程對其調用在jvm這裏是線性串行執行的,A中線程的監視器是this(當前對象),A中的監視器的解鎖 happens-before 與B線程的,若是同步中有兩個方法 a,b那麼ab的順序也是確認的。
因此說,時間前後順序與happens-before 原則沒有太大關係,咱們衡量並不是安全問題4的時候一切能夠依據線性發生原則爲準。
下一篇咱們着重聊聊happens-before的應用問題,歡迎來看。
-----------------------------------------------------------------------------
想看更多有趣原創的技術文章,掃描關注公衆號。
關注我的成長和遊戲研發,推進國內遊戲社區的成長與進步。