同步與線程間通信:
-
通信
通信是指消息在兩條線程之間傳遞。
既然要傳遞消息,那接收線程 和 發送線程之間必須要有個先后關系,此時就需要用到同步。通信和同步是相輔相成的。 -
同步
同步是指,控制多條線程之間的執行次序。
線程間通信方式:
- 共享內存
共享內存指的是多條線程共享同一片內存,發送者將消息寫入內存,接收者從內存中讀取消息,從而實現了消息的傳遞。
但這種方式有個弊端,即需要程序員來控制線程的同步,即線程的執行次序。
這種方式並沒有真正地實現消息傳遞,只是從結果上來看就像是將消息從一條線程傳遞到了另一條線程。
- 消息傳遞
顧名思義,消息傳遞指的是發送線程直接將消息傳遞給接收線程。
由於執行次序由並發機制完成,因此不需要程序員添加額外的同步機制,但需要聲明消息發送和接收的代碼。
java多線程內存模型:
所有線程都共享一片內存,用於存儲共享變量;
此外,每條線程都有各自的存儲空間,存儲各自的局部變量、方法參數、異常對象。
volatile的使用:
public volatile boolean flag;
1)volatile在重排序(編譯器、處理器在不改變程序執行結果的前提下,重新排列指令的執行順序,以達到最佳的運行效率)中的使用:
在以下情況下,即使兩行代碼之間沒有依賴關系,也不會發生重排序:
-
volatile讀
- 若volatile讀操作的前一行為volatile讀/寫,則這兩行不會發生重排序
- volatile讀操作和它后一行代碼都不會發生重排序
-
volatile寫
- volatile寫操作和它前一行代碼都不會發生重排序;
- 若volatile寫操作的后一行代碼為volatile讀/寫,則這兩行不會發生重排序。
volatile保證共享變量的內存可見性:
volatile修飾了一個成員變量后,這個變量的讀寫就會比普通變量多一些步驟。
-
volatile變量寫
當被volatile修飾的變量進行寫操作時,這個變量將會被直接寫入共享內存,而非線程的專屬存儲空間。 -
volatile變量讀
當讀取一個被volatile修飾的變量時,會直接從共享內存中讀,而非線程專屬的存儲空間中讀。
通過對volatile變量讀寫的限制,就能保證線程每次讀到的都是最新的值,從而確保了該變量的內存可見性。
volatile變量只能確保long、double讀寫的"原子性"(volatile在其他情況下是不能保證原子性的):
在Java中的所有類型中,有long、double類型比較特殊,他們占據8字節(64比特),其余類型都小於64比特。在32位操作系統中,CPU一次只能讀取/寫入32位的數據,因此對於64位的long、double變量的讀寫會進行兩步。在多線程中,若一條線程只寫入了long型變量的前32位,緊接着另一條線程讀取了這個只有“一半”的變量,從而就讀到了一個錯誤的數據。
為了避免這種情況,需要在用volatile修飾long、double型變量.
其實如果一個變量加了volatile關鍵字,就會告訴編譯器和JVM的內存模型:這個變量是對所有線程共享的、可見的,每次jvm都會讀取最新寫入的值並使其最新值在所有CPU可見。所以說的是線程可見性,沒有提原子性。
下面我們用一個例子說明volatile沒有原子性,不要將volatile用在getAndOperate場合(這種場合不原子,需要再加鎖,如i++),僅僅set或者get的場景是適合volatile的。
例如你讓一個volatile的integer自增(i++),其實要分成3步:1)讀取volatile變量值到local; 2)增加變量的值;3)把local的值寫回,讓其它的線程可見。這3步的jvm指令為:
mov 0xc(%r10),%r8d ; Load inc %r8d ; Increment mov %r8d,0xc(%r10) ; Store lock addl $0x0,(%rsp) ; StoreLoad Barrier
注意最后一步是內存屏障。
什么是內存屏障(Memory Barrier)?
內存屏障(memory barrier)是一個CPU指令。基本上,它是這樣一條指令: a) 確保一些特定操作執行的順序; b) 影響一些數據的可見性(可能是某些指令執行后的結果)。編譯器和CPU可以在保證輸出結果一樣的情況下對指令重排序,使性能得到優化。插入一個內存屏障,相當於告訴CPU和編譯器先於這個命令的必須先執行,后於這個命令的必須后執行。內存屏障另一個作用是強制更新一次不同CPU的緩存。例如,一個寫屏障會把這個屏障前寫入的數據刷新到緩存,這樣任何試圖讀取該數據的線程將得到最新值,而不用考慮到底是被哪個cpu核心或者哪顆CPU執行的。
內存屏障(memory barrier)和volatile什么關系?上面的虛擬機指令里面有提到,如果你的字段是volatile,Java內存模型將在寫操作后插入一個寫屏障指令,在讀操作前插入一個讀屏障指令。這意味着如果你對一個volatile字段進行寫操作,你必須知道:1、一旦你完成寫入,任何訪問這個字段的線程將會得到最新的值。2、在你寫入前,會保證所有之前發生的事已經發生,並且任何更新過的數據值也是可見的,因為內存屏障會把之前的寫入值都刷新到緩存。
為什么volatile變量用不同的線程訪問修改后訪問的結果會不一樣(多數情況下不建議使用volatile變量提供可見性):
JVM在運行時內存分配匯總有一個內存區域稱為虛擬機棧, 線程棧保存了線程運行時的信息,當線程訪問某個對象的值的時候,首先通過對象的引用找到對應在堆內存的變量的值,然后把堆內存變量的值load到本地內存中(當前線程所分配內存區域),建立一個變量副本, 之后線程不再和對象在堆內存的變量有任何聯系,而是直接修改副本的值,在修改完成之后自動把變量副本寫回堆內存,這樣堆內存的值就會改變:
read and load: 從主內存復制變量到當前工作內存
use and assign: 執行代碼, 改變共享變量
store and write: 用工作內存數據刷新主內存相關內容
但在read and load 之后, 如果線程1對該volatile變量修改還未結束, 線程2也進行修改, 但修改的是最初的值, 將會導致並發的發生.
volatile變量使用的場景:
1).對變量的寫入不依賴變量當前的值, 或者能確保只有單線程更新變量的值
2).該變量不會與其他狀態變量一起納入不變性條件中
3).在訪問變量時不需要加鎖
