一、保證內存可見性java
二、防止指令重排程序員
此外需注意volatile並不保證操做的原子性。安全
JVM內存模型:主內存和線程獨立的工做內存多線程
Java內存模型規定,對於多個線程共享的變量,存儲在主內存當中,每一個線程都有本身獨立的工做內存(好比CPU的寄存器),線程只能訪問本身的工做內存,不能夠訪問其它線程的工做內存。app
工做內存中保存了主內存共享變量的副本,線程要操做這些共享變量,只能經過操做工做內存中的副原本實現,操做完畢以後再同步回到主內存當中。ide
如何保證多個線程操做主內存的數據完整性是一個難題,Java內存模型也規定了工做內存與主內存之間交互的協議,定義了8種原子操做:性能
(1) lock:將主內存中的變量鎖定,爲一個線程所獨佔優化
(2) unclock:將lock加的鎖定解除,此時其它的線程能夠有機會訪問此變量spa
(3) read:將主內存中的變量值讀到工做內存當中線程
(4) load:將read讀取的值保存到工做內存中的變量副本中。
(5) use:將值傳遞給線程的代碼執行引擎
(6) assign:將執行引擎處理返回的值從新賦值給變量副本
(7) store:將變量副本的值存儲到主內存中。
(8) write:將store存儲的值寫入到主內存的共享變量當中。
經過上面Java內存模型的概述,咱們會注意到這麼一個問題,每一個線程在獲取鎖以後會在本身的工做內存來操做共享變量,操做完成以後將工做內存中的副本回寫到主內存,而且在其它線程從主內存將變量同步回本身的工做內存以前,共享變量的改變對其是不可見的。即其餘線程的本地內存中的變量已是過期的,並非更新後的值。
不少時候咱們須要一個線程對共享變量的改動,其它線程也須要當即得知這個改動該怎麼辦呢?下面舉兩個例子說明內存可見性的重要性:
有一個全局的狀態變量open:
1
|
boolean
open=
true
;
|
這個變量用來描述對一個資源的打開關閉狀態,true表示打開,false表示關閉,假設有一個線程A,在執行一些操做後將open修改成false:
1
2
3
|
<strong>
//線程A
resource.close();
open =
false
;
|
線程B隨時關注open的狀態,當open爲true的時候經過訪問資源來進行一些操做:
1
2
3
4
|
<strong>
//線程B
while
(open) {
doSomethingWithResource(resource);
}
|
當A把資源關閉的時候,open變量對線程B是不可見的,若是此時open變量的改動還沒有同步到線程B的工做內存中,那麼線程B就會用一個已經關閉了的資源去作一些操做,所以產生錯誤。
下面是一個經過布爾標誌判斷線程是否結束的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
public
class
CancelThreadTest {
publicstatic
void
main(String[] args)
throws
Exception{
PrimeGeneratorgen =
new
PrimeGenerator();
newThread(gen).start();
try
{
Thread.sleep(
3000
);
}
finally
{
gen.cancel();
}
}
}
class
PrimeGenerator
implements
Runnable{
privateboolean cancelled;
@Override
publicvoid run() {
while
(!cancelled)
{
System.out.println(
"Running..."
);
//doingsomething here...
}
}
publicvoid cancel(){cancelled =
true
;}
}
|
主線程中設置PrimeGenerator線程的是否取消標識,PrimeGenerator線程檢測到這個標識後就會結束線程,因爲主線程修改cancelled變量的內存可見性,主線程修改cancelled標識後並不立刻同步回主內存,因此PrimeGenerator線程結束的時間難以把控(最終是必定會同步回主內存,讓PrimeGenerator線程結束)。
若是PrimeGenerator線程執行一些比較關鍵的操做,主線程但願可以及時終止它,這時將cenceled用volatile關鍵字修飾就是必要的。
特別注意:上面演示這個並非正確的取消線程的方法,由於一旦PrimeGenerator線程中包含BolckingQueue.put()等阻塞方法,那麼將可能永遠不會去檢查cancelled標識,致使線程永遠不會退出。正確的方法參見另一篇關於如何正確終止線程的方法。
volatile保證可見性的原理是在每次訪問變量時都會進行一次刷新,所以每次訪問都是主內存中最新的版本。因此volatile關鍵字的做用之一就是保證變量修改的實時可見性。
針對上面的例子1:
要求一個線程對open的改變,其餘的線程可以當即可見,Java爲此提供了volatile關鍵字,在聲明open變量的時候加入volatile關鍵字就能夠保證open的內存可見性,即open的改變對全部的線程都是當即可見的。
針對上面的例子2:
將cancelled標誌設置的volatile保證主線程針對cancelled標識的修改可以讓PrimeGenerator線程立馬看到。
備註:也能夠經過提供synchronized同步的open變量的Get/Set方法解決此內存可見性問題,由於要Get變量open,必須等Set方徹底釋放鎖以後。後面將介紹到二者的區別。
指令重排序是JVM爲了優化指令,提升程序運行效率,在不影響單線程程序執行結果的前提下,儘量地提升並行度。編譯器、處理器也遵循這樣一個目標。注意是單線程。多線程的狀況下指令重排序就會給程序員帶來問題。
不一樣的指令間可能存在數據依賴。好比下面計算圓的面積的語句:
1
2
3
|
double
r =
2
.3d;
//(1)
double
pi =
3.1415926
;
//(2)
double
area = pi* r * r;
//(3)
|
area的計算依賴於r與pi兩個變量的賦值指令。而r與pi無依賴關係。
as-if-serial語義是指:無論如何重排序(編譯器與處理器爲了提升並行度),(單線程)程序的結果不能被改變。這是編譯器、Runtime、處理器必須遵照的語義。
雖然,(1) – happensbefore -> (2),(2) – happens before -> (3),可是計算順序(1)(2)(3)與(2)(1)(3) 對於r、pi、area變量的結果並沒有區別。編譯器、Runtime在優化時能夠根據狀況重排序(1)與(2),而絲絕不影響程序的結果。
指令重排序包括編譯器重排序和運行時重排序。
若是一個操做不是原子的,就會給JVM留下重排的機會。下面看幾個例子:
對於在同一個線程內,這樣的改變是不會對邏輯產生影響的,可是在多線程的狀況下指令重排序會帶來問題。看下面這個情景:
在線程A中:
1
2
|
context = loadContext();
inited =
true
;
|
在線程B中:
1
2
3
4
|
while
(!inited ){
//根據線程A中對inited變量的修改決定是否使用context變量
sleep(
100
);
}
doSomethingwithconfig(context);
|
假設線程A中發生了指令重排序:
1
2
|
inited =
true
;
context = loadContext();
|
那麼B中極可能就會拿到一個還沒有初始化或還沒有初始化完成的context,從而引起程序錯誤。
咱們都知道一個經典的懶加載方式的雙重判斷單例模式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public
class
Singleton {
private
static
Singleton instance =
null
;
private
Singleton() { }
public
static
Singleton getInstance() {
if
(instance ==
null
) {
synchronzied(Singleton.
class
) {
if
(instance ==
null
) {
<strong>instance =
new
Singleton();
//非原子操做
}
}
}
return
instance;
}
}
|
看似簡單的一段賦值語句:instance= new Singleton(),可是很不幸它並非一個原子操做,其實際上能夠抽象爲下面幾條JVM指令:
1
2
3
|
memory =allocate();
//1:分配對象的內存空間
ctorInstance(memory);
//2:初始化對象
instance =memory;
//3:設置instance指向剛分配的內存地址
|
上面操做2依賴於操做1,可是操做3並不依賴於操做2,因此JVM是能夠針對它們進行指令的優化重排序的,通過重排序後以下:
1
2
3
|
memory =allocate();
//1:分配對象的內存空間
instance =memory;
//3:instance指向剛分配的內存地址,此時對象還未初始化
ctorInstance(memory);
//2:初始化對象
|
能夠看到指令重排以後,instance指向分配好的內存放在了前面,而這段內存的初始化被排在了後面。
在線程A執行這段賦值語句,在初始化分配對象以前就已經將其賦值給instance引用,剛好另外一個線程進入方法判斷instance引用不爲null,而後就將其返回使用,致使出錯。
除了前面內存可見性中講到的volatile關鍵字能夠保證變量修改的可見性以外,還有另外一個重要的做用:在JDK1.5以後,可使用volatile變量禁止指令重排序。
解決方案:例子1中的inited和例子2中的instance以關鍵字volatile修飾以後,就會阻止JVM對其相關代碼進行指令重排,這樣就可以按照既定的順序指執行。
volatile關鍵字經過提供「內存屏障」的方式來防止指令被重排序,爲了實現volatile的內存語義,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。
相對於synchronized塊的代碼鎖,volatile應該是提供了一個輕量級的針對共享變量的鎖,當咱們在多個線程間使用共享變量進行通訊的時候須要考慮將共享變量用volatile來修飾。
volatile是一種稍弱的同步機制,在訪問volatile變量時不會執行加鎖操做,也就不會執行線程阻塞,所以volatilei變量是一種比synchronized關鍵字更輕量級的同步機制。
使用建議:在兩個或者更多的線程須要訪問的成員變量上使用volatile。當要訪問的變量已在synchronized代碼塊中,或者爲常量時,不必使用volatile。
因爲使用volatile屏蔽掉了JVM中必要的代碼優化,因此在效率上比較低,所以必定在必要時才使用此關鍵字。
一、volatile不會進行加鎖操做:
volatile變量是一種稍弱的同步機制在訪問volatile變量時不會執行加鎖操做,所以也就不會使執行線程阻塞,所以volatile變量是一種比synchronized關鍵字更輕量級的同步機制。
二、volatile變量做用相似於同步變量讀寫操做:
從內存可見性的角度看,寫入volatile變量至關於退出同步代碼塊,而讀取volatile變量至關於進入同步代碼塊。
三、volatile不如synchronized安全:
在代碼中若是過分依賴volatile變量來控制狀態的可見性,一般會比使用鎖的代碼更脆弱,也更難以理解。僅當volatile變量能簡化代碼的實現以及對同步策略的驗證時,才應該使用它。通常來講,用同步機制會更安全些。
四、volatile沒法同時保證內存可見性和原子性:
加鎖機制(即同步機制)既能夠確保可見性又能夠確保原子性,而volatile變量只能確保可見性,緣由是聲明爲volatile的簡單變量若是當前值與該變量之前的值相關,那麼volatile關鍵字不起做用,也就是說以下的表達式都不是原子操做:「count++」、「count = count+1」。
當且僅當知足如下全部條件時,才應該使用volatile變量:
一、 對變量的寫入操做不依賴變量的當前值,或者你能確保只有單個線程更新變量的值。
二、該變量沒有包含在具備其餘變量的不變式中。
總結:在須要同步的時候,第一選擇應該是synchronized關鍵字,這是最安全的方式,嘗試其餘任何方式都是有風險的。尤爲在、jdK1.5以後,對synchronized同步機制作了不少優化,如:自適應的自旋鎖、鎖粗化、鎖消除、輕量級鎖等,使得它的性能明顯有了很大的提高。