怎麼說呢?,最近看《深刻理解Java虛擬機 —— JVM高級特性與最佳實踐》這本書,這本書中也介紹可關於Java內存模型與線程、鎖等的問題,本篇就在此書之上作部分拓展,以便清楚的瞭解Java線程內存模型這個東東...java
經過上篇,你們瞭解到關於JVM這個東西,這章節主要介紹了其內存模型,內部的垃圾收集算法,在接下來會講的,請靜待... 本篇主要是介紹JMM - Java線程內存模型。算法
計算機中的內存模型緩存
物理計算機中的併發問題與虛擬中的狀況有很大的類似之處,物理機對併發的處理方案對於虛擬機的實現也有至關大的參考意義。安全
在現代計算機中,絕大多數的運算任務都須要靠處理器與內存交互實現,但交互時產生的 I/O 操做時間是沒法避免的,且 I/O 是很是須要時間的,沒法消除,如,讀取運算數據、存儲運算結果等。現代計算機中cpu的指令速度遠超內存的存取速度,計算機的存儲設備與處理器的運算速度有幾個數量級的差距,所以現代計算機系統都不得不加入一層讀寫速度儘量接近處理器運算速度的高速緩存(Cache)來做爲內存與處理器之間的緩衝,將運算須要使用到的數據複製到緩存中,讓運算能快速進行,當運算結束後再從緩存同步回內存之中,這樣處理器就無須等待緩慢的內存讀寫了。 多線程
基於Cache高速緩存的存儲交互很好地解決了處理器與內存的速度矛盾,但也帶來了更高的複雜度,一個新的問題:緩存一致性 ?併發
多處理器系統中,每一個處理器都有本身的高速緩存,而它們又共享同一主內存(Main Memory)。當多個處理器的運算任務都涉及同一塊主內存區域時,將可能致使各自的緩存數據不一致,那同步回到主內存時以誰的緩存數據爲準呢?app
爲了解決一致性的問題,須要各個處理器訪問緩存時都遵循一些協議,在讀寫時要根據協議來進行操做,這類協議有MSI、MESI(Illinois Protocol)、MOSI、Synapse、Firefly及Dragon Protocol等。框架
上圖中L一、L二、L3表明多級高速緩存,提供了數據的訪問性能,也減輕了數據總線上數據傳輸的壓力,同時也帶來了不少新的挑戰,Memory表示主內存。工具
Java線程內存模型(JMM)性能
不一樣的物理機器擁有不同的內存模型,而Java虛擬機也有本身的內存模型。
JMM是一系列的Java虛擬機平臺對開發者提供的多線程環境下的內存可見性、是否能夠重排序等問題的無關具體平臺的統一的保證。
在不一樣的硬件生產商與不一樣的操做系統下,內存的訪問邏輯有必定的差別,結果就是當你的代碼在某個系統環境下運行良好,而且線程安全,可是換了個系統就出現各類問題。Java內存模型,就是爲了屏蔽系統和硬件的差別,讓一套代碼在不一樣平臺下能到達相同的訪問結果。JMM從java 5開始的JSR-133發佈後,已經成熟和完善起來。
注:JSR-133,Java Memory Model and Thread specification revision,Java內存模型與線程規範修訂。
從抽象的角度來看,JMM定義了線程和主內存之間的抽象關係:線程之間的共享變量存儲在主內存(Main Memory)中,每一個線程都有一個私有的本地內存(Local Memory),本地內存中存儲了該線程以讀/寫共享變量的副本。本地內存是JMM的一個抽象概念,並不真實存在。它涵蓋了緩存、寫緩衝區、寄存器以及其餘的硬件和編譯器優化。
Java語言規範中提到過,JVM中存在一個主存區(Main Memory或Java Heap Memory),Java中全部變量都是存在主存中的,對於全部線程進行共享,而每一個線程又存在本身的工做內存(Working Memory),工做內存中保存的是主存中某些變量的拷貝(副本),線程對全部變量的操做並不是發生在主存區,而是發生在工做內存中,而線程之間是不能直接相互訪問,變量在程序中的傳遞,是依賴主存來完成的。
疑問:副本拷貝?若線程中訪問一個超大的對象,那也會將其進行拷貝嗎?
答:不會拷貝對象的,該對象的引用、對象中某個線程訪問的字段有可能進行拷貝,可是不會把整個對象進行拷貝的。
注:JMM是一個抽象的概念,而JVM也存在部分抽象概念,所以JMM與JVM是不存在映射關係的,若是強扯上關係,那就以本身理解爲主。Java內存模型只是抽象出來的,與物理內存的對應關係在實際運行中,主內存和工做內存可能都處於物理機的主存中。
內存間交互操做
那Java 線程之間的對象在內存中如何進行操做呢?(參考深刻《理解Java虛擬機》)
關於主內存與工做內存之間的具體交互,即一個變量如何從主內存拷貝到工做內存? 又如何從工做內存同步到主內存呢?
內存交互操做有8種,虛擬機實現必須保證每個操做都是原子的,不可在分的(對於double和long類型的變量來講,load、store、read和write操做在某些平臺上容許例外)
lock (鎖定):做用於主內存的變量,把一個變量標識爲線程獨佔狀態;
unlock (解鎖):做用於主內存的變量,它把一個處於鎖定狀態的變量釋放出來,釋放後的變量才能夠被其餘線程鎖定;
read (讀取):做用於主內存變量,它把一個變量的值從主內存傳輸到線程的工做內存中,以便隨後的load動做使用;
load (載入):做用於工做內存的變量,它把read操做從主存中變量放入工做內存中;
use (使用):做用於工做內存中的變量,它把工做內存中的變量傳輸給執行引擎,每當虛擬機遇到一個須要使用到變量的值,就會使用到這個指令;
assign (賦值):做用於工做內存中的變量,它把一個從執行引擎中接受到的值放入工做內存的變量副本中;
store (存儲):做用於主內存中的變量,它把一個從工做內存中一個變量的值傳送到主內存中,以便後續的write使用;
write (寫入):做用於主內存中的變量,它把store操做從工做內存中獲得的變量的值放入主內存的變量中;
若是要把一個變量從主內存賦值到工做內存,須要順序的執行read ---> load操做; 若是要把變量從工做內存同步到主內存中,則須要順序的執行store --->write操做;
注:---> 只表明順序,該符號不保證連續性,中間能夠有其它執行操做。
JMM對這八種指令的使用,制定了以下規則:
不容許read和load、store和write操做之一單獨出現。即便用了read必須load,使用了store必須write;
不容許線程丟棄他最近的assign操做,即工做變量的數據改變了以後,必須告知主存;
不容許一個線程將沒有assign的數據從工做內存同步回主內存;
一個新的變量必須在主內存中誕生,不容許工做內存直接使用一個未被初始化的變量。就是懟變量實施use、store操做以前,必須通過assign和load操做;
一個變量同一時間只有一個線程能對其進行lock。屢次lock後,必須執行相同次數的unlock才能解鎖;
若是對一個變量進行lock操做,會清空全部工做內存中此變量的值,在執行引擎使用這個變量前,必須從新load或assign操做初始化變量的值;
若是一個變量沒有被lock,就不能對其進行unlock操做。也不能unlock一個被其餘線程鎖住的變量;
對一個變量進行unlock操做以前,必須把此變量同步回主內存;
JMM對這八種操做規則和對volatile的一些特殊規則就能肯定哪裏操做是線程安全,哪些操做是線程不安全的了。可是這些規則實在複雜,很難在實踐中直接分析。因此通常咱們也不會經過上述規則進行分析。更多的時候,使用java的happen-before規則來進行分析。
Java內存模型帶來的問題
可見性問題:上面所述中有一個拷貝副本的操做,那某一線程將值修改後,如何進行同步到其餘線程的值中呢?
線程競爭問題:兩個線程對某個值進行操做後,都會對主內存中的值進行從新賦值,那此時新的值結果並不是是準確的,如何保證線程執行的結果一致性呢?
重排序問題:Java 內存模型還會對指令進行重排序操做,在執行程序時爲了提升性能編譯器和處理器常常會對指令進行重排序操做。
Volatile修飾的特殊規則
volatile關鍵字主要是Java虛擬機提供的最輕量級的同步機制。此時能夠藉助synchronized來配合使用。
當一個變量被volatile修飾後,它將具有兩個特性:可見性、禁止指令重排。(記住呀,這兒沒有原子性的)
可見性:假如在多線程的場景下,某一線程修改了變量的值,那麼這個新的值對其餘全部線程來講是當即得知的。
禁止指令重排:在程序運行過程當中,只保證結果最終的一致性,但在編譯時,會對代碼進行優化,而此時不知足 '先行發生' 原則,編譯器會自行進行排序優化,而volatile關鍵字禁止指令重排,保證有序性。
具體實現:(這兒參考《深刻理解Java虛擬機》,查看對應的字節碼文件,未找到相關明顯的區別,除了volatile),《深刻理解Java虛擬機》中有一句話:「觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的彙編代碼發現,加入volatile關鍵字時,會多出一個lock前綴指令」,lock前綴指令生成一個內存屏障(Memory Banrrier)。保證重排序後的指令不會越過內存屏障,實現對內存操做的順序限制,即volatile以前的代碼只會在volatile以前執行,volaiter以後的代碼只會在volatile以後執行。
注:Lock前綴的做用:使本CPU的Cache寫入內存,該寫入動做也會引發別的CPU或別的內核無效化,好比以前的多線程對num進行++操做;同時指令重排序沒法越過內存屏障,保證指令的有序。
上面提到保證有序性,除了volatile以外,還有synchronized、final等關鍵字。
Volatile的使用場景之一是:在DCL(DOuble-Check-Lock)雙重校驗鎖的單例對象建立其實是一種延遲初始化的技巧,爲建立的對象的變量使用volatile來修飾,保證線程之間的該對象的可見性。
什麼是內存屏障呢?如何實現?
待完善...
先行發生原則
常規的開發中,判斷數據是否存在競爭,線程是否安全,都須要依據Happens-Before原則,也就是Java內存模型當中定義的兩項操做之間的偏序關係。意思就是當A操做先行發生於B操做,則在發生B操做的時候,操做A產生的影響能被B觀察到,「影響」包括修改了內存中的共享變量的值、發送了消息、調用了方法等。
Happen-Before的規則有如下幾條:
程序次序規則(Program Order Rule):在一個線程內,程序的執行規則跟程序的書寫規則是一致的,從上往下執行。
管程鎖定規則(Monitor Lock Rule):一個Unlock的操做確定先於下一次Lock的操做。這裏必須是同一個鎖。同理咱們能夠認爲在synchronized同步同一個鎖的時候,鎖內先行執行的代碼,對後續同步該鎖的線程來講是徹底可見的。
volatile變量規則(volatile Variable Rule):對同一個volatile的變量,先行發生的寫操做,確定早於後續發生的讀操做
線程啓動規則(Thread Start Rule):Thread對象的start()方法先行發生於此線程的沒一個動做
線程停止規則(Thread Termination Rule):Thread對象的停止檢測(如:Thread.join(),Thread.isAlive()等)操做,必行晚於線程中全部操做
線程中斷規則(Thread Interruption Rule):對線程的interruption()調用,先於被調用的線程檢測中斷事件(Thread.interrupted())的發生
對象停止規則(Finalizer Rule):一個對象的初始化方法先於一個方法執行Finalizer()方法
傳遞性(Transitivity):若是操做A先於操做B、操做B先於操做C,則操做A先於操做C
以上就是Happen-Before中的規則,Java無需任何同步手段保障,就能夠成立的先行發生原則就是上面的幾個了。經過這些條件的斷定,仍然很難判斷一個線程是否能安全執行,畢竟在咱們的時候線程安全多數依賴於工具類的安全性來保證。想提升本身對線程是否安全的判斷能力,必然須要理解所使用的框架或者工具的實現,並積累線程安全的經驗。
那一個操做,在 '時間' 上先發生,可否說這個操做是 '先行發生' 呢? 再者,一個操做是 '先行發生' ,那可否說這個操做是 '時間上先發生' 呢?
答案就是不,兩個都是不能夠。時間前後順序與先行發生原則之間基本沒有太大關係,衡量併發線程是否安全問題,一切以先行發生原則爲準。
(願你的每一行代碼,都有讓世界進步的力量 ------ fn)