Java內存模型(JMM)
原子性(Atomicity)
原子性是指一個操作是不可中斷的。即使是多個線程一起執行的時候,一個操作一旦開始,就不會被其他線程干擾。
比如,一個靜態全局變量 int i,兩個線程同時對它賦值,線程A給他賦值1,線程B給它賦值-1。那麼不管兩個線程怎麼工作,i的值只能是1或則-1.線程A和線程B之間沒有干擾。
對於 32
位系統的來說, long
類型數據和 double
類型數據(對於基本數據類型, byte
, short
, int
, float
, boolean
, char
讀寫是原子操作),它們的讀寫並非原子性的
指令重排
計算機在執行程序時,爲了提高性能,編譯器和處理器的常常會對指令做重排。指令重排可以保證串行語義一致,但是沒有義務保證多線程間的語義也一致。
編譯器優化的重排
編譯器重排
編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執行順序。
指令並行的重排
處理器重排
現代處理器採用了指令級並行技術來將多條指令重疊執行。如果不存在數據依賴性(即後一個執行的語句無需依賴前面執行的語句的結果),處理器可以改變語句對應的機器指令的執行順序
內存系統的重排
處理器重排
由於處理器使用緩存和讀寫緩存衝區,這使得加載(load)和存儲(store)操作看上去可能是在亂序執行,因爲三級緩存的存在,導致內存與緩存的數據同步存在時間差。
編譯器重排
有兩個線程如下:
Thread 1 Thread2
1:r2=A; 3:r1=B;
2:B=1; 4:A=2;
上述兩個線程同時執行,分別有1、2、3、4四段執行代碼,其中1、2屬於線程1 , 3、4屬於線程2 ,從程序的執行順序上看,似乎不太可能出現r1 = 1 和r2 = 2 的情況,但實際上這種情況是有可能發現的,因爲如果編譯器對這段程序代碼執行重排優化後,可能出現下列情況:
Thread 1 Thread2
2:B=1; 4:A=2;
1:r2=A; 3:r1=B;
這種執行順序下就有可能出現 r1 = 1
和 r2 = 2
的情況,這也就說明在 多線程環境下,由於編譯器優化重排的存在,兩個線程中使用的變量能否保證一致性是無法確定的
。
處理器重排
處理器指令重排是對CPU的性能優化,從指令的執行角度來說一條指令可以分爲多個步驟完成,如下:
- 取指 IF
- 譯碼和取寄存器操作數 ID
- 執行或者有效地址計算 EX
- 存儲器訪問 MEM
- 寫回 WB
可見性(Visibility)
可見性是指當一個線程修改了某一個共享變量的值,其他線程是否能夠立即知道這個修改。
對於串行程序來說,可見性問題是不存在的。因爲你在任何一個操作中修改了某個變量,下個操作讀取這個變量的值,一定是修改後的新值。
但在多線程環境中可就不一定了,由於線程對共享變量的操作都是線程拷貝到各自的工作內存進行操作後才寫回到主內存中的,這就可能存在一個線程A修改了共享變量x的值,還未寫回主內存時,另外一個線程B又對主內存中同一個共享變量x進行操作,但此時A線程工作內存中共享變量x對線程B來說並不可見,這種工作內存與主內存同步延遲現象就造成了可見性問題。
指令重排以及編譯器優化也可能導致可見性問題。
有序性(Ordering)
有序性是指對於單線程的執行代碼,我們總是認爲代碼的執行是按順序依次執行的。
對於多線程環境,則可能出現亂序現象,因爲程序編譯成機器碼指令後可能會出現指令重排現象,重排後的指令與原指令的順序未必一致,要明白的是,在Java程序中,倘若在本線程內,所有操作都視爲有序行爲,如果是多線程環境下,一個線程中觀察另外一個線程,所有操作都是無序的
,前半句指的是單線程內保證串行語義執行的一致性,後半句則指指令重排現象和工作內存與主內存同步延遲現象。
JMM提供的解決方案
原子性問題
除了
JVM
自身提供的對基本數據類型讀寫操作的原子性外,對於方法級別或者代碼塊級別的原子性操作,可以使用synchronized
關鍵字或者重入鎖(ReentrantLock
)` 保證程序執行的原子性工作內存與主內存同步延遲現象導致的可見性問題
可以使用
synchronized
關鍵字或者volatile
關鍵字解決,它們都可以使一個線程修改後的變量立即對其他線程可見。對於指令重排導致的可見性問題和有序性問題
可以利用
volatile
關鍵字解決,因爲volatile
的另外一個作用就是禁止重排序優化。happens-before
原則
JMM中的 happens-before 原則
雖然JVM和執行系統會對指令進行一定的重排,但是指令重排是有原則的(減少程序開發的複雜性),並非所有的指令都可以隨便改變執行位置。
程序順序原則
即在一個線程內必須保證語義串行性,也就是說按照代碼順序執行。
鎖規則
解鎖(unlock)操作必然發生在後續的同一個鎖的加鎖(lock)之前,也就是說,如果對於一個鎖解鎖後,再加鎖,那麼加鎖的動作必須在解鎖動作之後(同一個鎖)。
volatile規則
volatile變量的寫,先發生於讀,這保證了volatile變量的可見性,簡單的理解就是,volatile變量在每次被線程訪問時,都強迫從主內存中讀該變量的值,而當該變量發生變化時,又會強迫將最新的值刷新到主內存,任何時刻,不同的線程總是能夠看到該變量的最新值。
線程啓動規則
線程的start()方法先於它的每一個動作,即如果線程A在執行線程B的start方法之前修改了共享變量的值,那麼當線程B執行start方法時,線程A對共享變量的修改對線程B可見
傳遞性
A先於B ,B先於C 那麼A必然先於C
線程終止規則
線程的所有操作先於線程的終結,Thread.join()方法的作用是等待當前執行的線程終止。假設在線程B終止之前,修改了共享變量,線程A從線程B的join方法成功返回後,線程B對共享變量的修改將對線程A可見。
線程中斷規則
對線程 interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生,可以通過Thread.interrupted()方法檢測線程是否中斷。
對象終結規則
對象的構造函數執行,結束先於finalize()方法