聲明:原創作品,轉載時請注明文章來自SAP師太技術博客( 博/客/園www.cnblogs.com):m.survivalescaperooms.com/jiangzhengjun,并以超鏈接形式標明文章原始出處,否則將追究法律責任!原文鏈接:http://m.survivalescaperooms.com/jiangzhengjun/p/4255698.html 第十章 并發 66、 同步訪問共享的可變數據 許多程序員把同步的概念僅僅理解為一個種互斥的方式,即,當一個對象被一個線程修改的時候,可以阻止另一個線程觀察到對象的內部不一致的狀態。正確地使用同步可以保證其他任何方法都不會看到對象處于不一致的狀態中。這種觀點是正確的,但是它并沒有說明同步的全部意義。如果沒有同步,一個線程的變化就不能被其他線程看到。同步不僅可以阻止一個線程看到對象處于不一致的狀態中(即原子性),它還可以保證進入同步方法或者同步代碼塊的每個線程,都看到由同一個鎖保護的之前所有的修改結果(即可見性)。
我的理解,同步=原子性+可見性
synchronized就是同步的代名詞,它具有原子性與可見性。而volatile只具有可見性,但不具有原子性。可見性其實說的就是在讀之前與寫之后都與主內同步,除了可見性外,volatile還嚴禁語義重排:“禁止reorder任意兩個volatile字段或者volatile變量,并且同時嚴格限制(盡管沒有禁止)reorder volatile字段(或變量)周圍的非volatile字段(或變量)。”
Java語言規范保證或寫是一個變量是原子的(即數據的讀寫是不可分割的。注,不可分割的操作并不意味“多線程安全”),除非這個變量的類型為long或double[JLS 17.4.7]。換句話說,讀取一個非long或double類型的變量,可以保證返回值是某個線程完整保存在該變量中的值(即要么讀取還沒有修改的值,要么讀取到某線程修改完后的值,但決不會讀到另一線程對變量的一半或一部分修改后的值,如一個int型變量,某線修改該變量的前16位后,被另一線程讀到,這是不可能的;而long或double類型的變量就完全有可能這樣,讀到的是另一線程寫入的高32位,而低32位還是原來值),即使用多個線程在沒有同步的情況下并發地修改這個變量也是如此。
你可能聽說過,為了提高性能,在讀或寫原子數據的時候,應該避免使用同步。這個建議是非常危險而錯誤的。雖然語言規范保證了線程在讀取原子數據的時候,不會看到任意的數值(嚴格的說是完整的值,即不會讀取還未修改完成的值),但是它并不保證一個線程寫入的值對于另一個線程將是可見的(即另一線程修改完后,其他線程有可能將永遠讀不到這個修改后的值)。為了在線程之間進行可靠的通信(需要靠可見性來保證),也為了互斥訪問(需要原子性來保證),同步(需要可見性和原子性來保證)是必要的。這歸因于Java語言規范中內存模型,它規定了一個線程所做的變化何時以及如何讓其他線程可見[JLS 17]。
如果對共享的可變數據的訪問不能同步,其后果將非常可怕,即使這個變量是原子可讀寫的。考慮下面這個阻止一個線程妨礙另一個線程的任務。由于boolean域的讀和寫操作都是原子的,程序員在訪問這個域的時候不再使用同步,這是錯誤的做法:
importjava.util.concurrent.TimeUnit;
publicclassStopThread {
PRivatestaticbooleanstopRequested;
publicstaticvoidmain(String[] args)throwsInterruptedException {
Thread backgroundThread =newThread(newRunnable() {
publicvoidrun() {
inti = 0;
while(!stopRequested)
i++;
}
});
backgroundThread.start();
//睡一秒
TimeUnit.SECONDS.sleep(1);
stopRequested=true;
}
}
你可能期待這個程序運行大約一秒鐘之后,主線程將stopRequested設置為true,致使后臺線程的循環終止。但是在我的機子上,這個程序永遠不會終止:因為后臺線和永遠在循環中!
問題在于,由于沒有同步,就不能保證后臺線程何時“看到”主線程對stopRequested的值所做的修改。在沒有同步的情況下,VM將個這個代碼:
while(!stopRequested)
i++;
轉變成這樣:
if(!stopRequested)
while(true)
i++;
這是完全有可能的,也是可以接受的。這種優化稱作提升(hoisting),正是HopSpot Server VM的工作。結果是個“活性失敗”:這個程序無法結束。修改這個問題的一種方式是同步訪問stopRequested域,修改如下:
publicclassStopThread {
privatestaticbooleanstopRequested;
privatestaticsynchronizedvoidrequestStop() {
stopRequested=true;
}
privatestaticsynchronizedbooleanstopRequested() {
returnstopRequested;
}
publicstaticvoidmain(String[] args)throwsInterruptedException {
Thread backgroundThread =newThread(newRunnable() {
publicvoidrun() {
inti = 0;
while(!stopRequested())
i++;
}
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
requestStop();
}
}
注意上面的寫方法(requestStop)和讀方法(stopRequested)都被同步了,只同步寫方法或讀方法是不夠的!
StopThread程序中被同步方法的動作即使沒有同步也是原子的。換句話說,這些方法的同步只是為了它的通信效果(即可見性),而不是為了互斥訪問(即原子性)。雖然循環的每個迭代中的同步開銷很小,還是有其他更正確的替代方法,它更加簡潔,性能也可能更好。這種替代就是將stopRequested聲明為volatile,第二版本的StopThread中的鎖就可以省略。雖然volatile修飾符不具有互斥訪問的特性,但它可以保證任何一個線程在讀取該域的時候都將看到最近剛剛被其他線程寫入的值,下面是使用volatile修正后的版本:
publicclassStopThread {
privatestaticvolatilebooleanstopRequested;
publicstaticvoidmain(String[] args)throwsInterruptedException {
Thread backgroundThread =newThread(newRunnable() {
publicvoidrun() {
inti = 0;
while(!stopRequested)
i++;
}
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested=true;
}
}
上面就說了,volatile只具有可見性,而不具有原子性,所以使用時要格外小心,請考慮下面的方法,假設它要產生序列號:
privatestaticvolatileintnextSerialNumber= 0;
publicstaticintgenerateSerialNumber() {
returnnextSerialNumber++;
}
這個方法的目的是要確保每次調用都要返回不同的值,而且是遞增的(只要不超過2^32次調用)。這個方法的狀態只包含一個可原子訪問的域:nextSerialNumber,不同步的情況下讀到的這個域的所有可能的值都是合法(即不可能讀到修改未完成的值),但是,這個方法仍然無法工作。
問題在于,增量操作(++)不是原子的。它在nextSerialNumber域中執行兩項操作:首先它讀取值,然后寫回一個新值,相當于原來的值再加上1。如果第二個線程在第一個線程讀取舊值和寫回新值期間讀取這個域,第二個線程就會與第一個線程一起看到同一個值,并返回相同的序列號。這就是“安全性失敗”:這個程序會計算出錯誤的結果。
修正generateSerialNumber方法的一種方法是是在它的聲明中加上synchronized修飾符。這樣可能確保多個調用不會交叉存在。一旦這么做,就可以且應該從nextSerialNumber中刪除volatile修飾符。為了讓這個方法更可靠,要用long代替int。但最好還是遵循第47條中的建議,使用類AtomicLong,它是java.util.concurrent.atomic的一部分,它比同步版本的generateSerialNumber性能上可能要更好,因為atomic包使用了非鎖定的線程安全技術來做到同步的,下面是使用AtomicLong修正后的版本:
privatestaticfinalAtomicLongnextSerialNum=newAtomicLong();
publicstaticlonggenerateSerialNumber() {
returnnextSerialNum.getAndIncrement();
}
避免本條目中所討論到的問題的最佳辦法是不共享可變的數據,要么共享不可變的數據(見第15條),要么壓根不共
新聞熱點
疑難解答