在多線程編程中,線程之間如何通訊和同步是一個必須解決的問題:java
線程之間有兩種通訊的方式:消息傳遞和共享內存程序員
num
和Lock
能夠被理解爲公共狀態同步指程序中永固空值不一樣線程間的操做發生的相對順序的機制編程
Lock
加鎖和解鎖之間的代碼塊,或者被synchronized
包圍的代碼塊Objetc#notify()
,喚醒的線程接收信號必定在發送喚醒信號的發送以後。在java中,全部的實例域,靜態域、數組都被存儲在堆空間當中,堆內存在線程之間共享。c#
全部的局部變量,方法定義參數和異常處理器參數不會被線程共享,在每一個線程棧中獨享,他們不會存在可見性和線程安全問題。數組
從Java線程模型(JMM)的角度來看,線程之間的共享變量存儲在主內存當中,每一個線程擁有一個私有的本地內存(工做內存)本地內存存儲了該線程讀——寫共享的變量的副本。
JMM只是一個抽象的概念,在現實中並不存在,其中全部的存儲區域都在堆內存當中。JMM的模型圖以下圖所示:
緩存
而java線程對於共享變量的操做都是對於本地內存(工做內存)中的副本的操做,並無對共享內存中原始的共享變量進行操做;安全
以線程1和線程2爲例,假設線程1修改了共享變量,那麼他們之間須要通訊就須要兩個步驟:多線程
爲了完成這一線程之間的通訊,JMM爲內存間的交互操做定義了8個原子操做,以下表:併發
操做 | 做用域 | 說明 |
---|---|---|
lock(鎖定) | 共享內存中的變量 | 把一個變量標識爲一條線程獨佔的狀態 |
unlock(解鎖) | 共享內存中的變量 | 把一個處於鎖定的變量釋放出來,釋放後其餘線程能夠進行訪問 |
read(讀取) | 共享內存中的變量 | 把一個變量的值從共享內存傳輸到線程的工做內存。供隨後的load操做使用 |
load(載入) | 工做內存 | 把read操做從共享內存中獲得的變量值放入工做內存的變量副本當中 |
use(使用) | 工做內存 | 把工做內存中的一個變量值傳遞給執行引擎 |
assign(賦值) | 工做內存 | 把一個從執行引擎接受到的值賦值給工做內存的變量 |
store(存儲) | 做用於工做內存 | 把一個工做內存中的變量傳遞給共享內存,供後續的write使用 |
write(寫入) | 共享內存中的變量 | 把store操做從工做內存中獲得的變量的值放入主內存 |
JMM規定JVM四線時必須保證上述8個原子操做是不可再分割的,同時必須知足如下的規則:app
read
和load
、store
和write
操做之一單獨出現,即不容許只從共享內存讀取但工做內存不接受,或者工做捏村發起回寫可是共享內存不接收assign
操做,即當一個線程修改了變量後必須寫回工做內存和共享內存lock
操做,可是一個變量能夠被同一個線程重複執行屢次lock
,可是須要相同次數的unlock
lock
操做,那麼會清空工做內存中此變量的值,在執行引擎使用這個變量以前須要從新執行load和assignunlock
一個沒有被鎖定的變量,也不容許unlock
一個其餘線程lock
的變量unlock
以前必須把此變量同步回主存當中。對
long
和double
的特殊操做
在一些32位的處理器上,若是要求對64位的long
和double
的寫具備原子性,會有較大的開銷,爲了照固這種狀況,
java語言規範鼓勵但不要求虛擬機對64位的long
和double
型變量的寫操做具備原子性,當JVM在這種處理器上運行時,
可能會把64位的long和double拆分紅兩次32位的寫
爲了保證內存的可見性,JMM的編譯器會禁止特定類型的編譯器從新排序;對於處理器的從新排序,
JMM會要求編譯器在生成指令序列時插入特定類型的的內存屏障指令,經過內存屏障指令巾紙特定類型的處理器從新排序
JMM規定了四種內存屏障,具體以下:
屏障類型 | 指令示例 | 說明 |
---|---|---|
LoadLoad Barriers | Load1;LoadLoad;Load2 | 確保Load1的數據先於Load2以及全部後續裝在指令的裝載 |
StoreStore Barries | Store1;StoreStore;Store2 | 確保Store1數據對於其餘處理器可見(刷新到內存)先於Store2及後續存儲指令的存儲 |
LoadStore Barriers | Load1;LoadStore;Store2 | 確保Load1的裝載先於Store2及後續全部的存儲指令 |
StoreLoad Barrier | Store1;StoreLoad;Load2 | 確保Store1的存儲指令先於Load1以及後續所全部的加載指令 |
StoreLoad
是一個「萬能」的內存屏障,他同時具備其餘三個內存屏障的效果,現代的處理器大都支持該屏障(其餘的內存屏障不必定支持),
可是執行這個內存屏障的開銷很昂貴,由於須要將處理器緩衝區全部的數據刷回內存中。
在JSR-133種內存模型種引入了happens-before
規則來闡述操做之間的內存可見性。在JVM種若是一個操做的結果過須要對另外一個操做可見,
那麼兩個操做之間必然要存在happens-bsfore關係:
- 程序順序規則:一個線程中的個每一個操做,happens-before於該線程的後續全部操做
- 監視器鎖規則:對於一個鎖的解鎖,happens-before於隨後對於這個鎖的加鎖
- volatitle變量規則:對於一個volatile的寫,happens-before於認意後續對這個volatile域的讀
- 傳遞性:若是A happens-before B B happends-beforeC,那麼A happends-before C
衆所周知,JVM執行的是字節碼,Java源代碼須要先編譯成字節碼程序才能在Java虛擬機中運行,可是考慮下面的程序;
int a = 1; int b = 1;
在這段代碼中,a
和b
沒有任何的相互依賴關係,所以徹底能夠先對b
初始化賦值,再對a
變量初始化賦值;
事實上,爲了提升性能,編譯器和處理器一般會對指令作從新排序。重排序分爲3種:
數據依賴性:若是兩個操做訪問同一個變量,且兩個操做有一個是寫操做,則這兩個操做存在數據依賴性,改變這兩個操做的執行順序,就會改變執行結果。
儘管指令重排序會提升代碼的執行效率,可是卻爲多線程編程帶來了問題,多線程操做共享變量須要必定程度上遵循代碼的編寫順序,
也須要將修改的共享數據存儲到共享內存中,不按照代碼順序執行可能會致使多線程程序出現內存可見性的問題,那又如何實現呢?
as-if-serial
語義:不論程序怎樣進行重排序,(單線程)程序的執行結果不能被改變。編譯器、runtime
和處理器都必須支持as-if-serial
語義。
假設存在如下happens-before
程序規則:
1) A happens-before B 2) B happens-before C 3) A happens-before C
儘管這裏存在A happens-before B
這一關係,可是JMM並不要求A
必定要在B
以前執行,僅僅要求A
的執行結果對B
可見。
即JMM僅要求前一個操做的結果對於後一個操做可見,而且前一個操做按照順序排在後一個操做以前。
可是若前一個操做放在後一個操做以後執行並不影響執行結果,則JMM認爲這並不違法,JMM容許這種重排序。
在一個線程中寫一個變量,在另外一個線程中同時讀取這個變量,讀和寫沒有經過排序來同步來排序,就會引起數據競爭。
數據競爭的核心緣由是程序未正確同步。若是一個多線程程序是正確同步的,這個程序將是一個沒有數據競爭的程序。
順序一致性模型只是一個參考模型。
在順序一致性模型中每一個曹祖都必須原子執行且馬上對全部線程可見。
當線程未正確同步時,JMM只提供最小的安全性,當讀取到一個值時,這個值要麼是以前寫入的值,要麼是默認值。
JMM保證線程的操做不會無中生有。爲了保證這一特色,JMM在分配對象時,首先會對內存空間清0,而後纔在上面分配對象。
未同步的程序在JMM種執行時,總體上是無序的,執行結果也沒法預知。位同步程序子兩個模型中執行特色有以下幾個差別:
long
和double
型變量具備寫操做的原子性,而順序一致性模型保證對全部的內存的讀/寫操做都具備原子性java併發編程的藝術-方騰飛,魏鵬,程曉明著