366
外匯
可見性問題實例
說到並發安全時,我們常提及可見性的問題,通俗點講就是線程1看不到線程2寫入變量v的值(更專業的解釋以及是什麼導致可見性問題,又該如何解決,見擴展閱讀),但一直偏於理論,實際中有沒有因可見性而導致問題的例子呢?回答是肯定的,接下來我們一起來看幾個例子。
這個例子很簡單,新建的線程裏有一個普通變量stop,用來表示是否結束循環裏的自增操作。主線程啟動這個線程後,將該變量置為true,觀察線程
是否打印出finish loop那行,如果存在可見性問題,主線程修改stop值為true,線程v看stop的值應該還是false。
01 |
class VisibilityThread extends Thread {
|
06 |
System.out.println( "start loop." );
|
10 |
System.out.println( "finish loop,i=" + i);
|
13 |
public void stopIt() {
|
17 |
public boolean getStop(){
|
22 |
public class VisibilityTest {
|
23 |
public static void main(String[] args) throws Exception {
|
24 |
VisibilityThread v = new VisibilityThread();
|
27 |
Thread.sleep( 1000 ); //停頓1秒等待新啟線程執行
|
28 |
System.out.println( "即將置stop值為true" );
|
31 |
System.out.println( "finish main" );
|
32 |
System.out.println( "main中通過getStop獲取的stop值:" + v.getStop());
|
我們先來執行一遍(操作係統:XP,下同。JDK:見圖示):

執行結果如上圖,有人該問了,線程v最終停下來了,這不是表示它看到stop值為true了嗎?是的,確實如此。但讓我們再看一個這個程序的執行結果。

這一次,我們發現程序一直未能結束,表示線程v看到stop的值是false,但是主線程打印出的值卻是true。
對比兩次的執行方式,我們發現後一次加上了-server選項。顯示version的時候也由Client VM變成了Server
VM。那麼Client VM與Server VM有什麼區別在哪裏?簡單地講,Client
VM啟動時做了一般優化,耗時少,啟動快,但程序執行的也相對也較慢;Server
VM啟動的時候做了更多優化,耗時多,啟動慢,但程序執行快。如果在運行java命令的時候沒有指定具體模式的時候,會有一個默認值,這個默認值隨硬件和
操作係統的不同而不同,這裏有張JDK 1.6在各平台默認VM模式的圖。
我們再來看個例子,這個例子源於hotspot VM的一個bug:
01 |
public class InterruptedVisibilityTest {
|
03 |
System.out.println( "新線程正在執行" );
|
05 |
if (checkInterruptedStatus()) break ;
|
07 |
System.out.println( "新線程退出循環" );
|
10 |
private boolean checkInterruptedStatus() {
|
11 |
return Thread.currentThread().isInterrupted();
|
14 |
public static void main(String[] args) throws Exception {
|
15 |
final InterruptedVisibilityTest test = new InterruptedVisibilityTest();
|
16 |
Thread thinkerThread = new Thread( "Thinker" ) {
|
21 |
thinkerThread.start();
|
22 |
Thread.sleep( 1000 ); //等待新線程執行
|
23 |
System.out.println( "馬上中斷thinkerThread" );
|
24 |
thinkerThread.interrupt();
|
25 |
System.out.println( "已經中斷thinkerThread" );
|
26 |
thinkerThread.join( 3000 );
|
27 |
if (thinkerThread.isAlive()) {
|
28 |
System.err.println( "thinkerThread未能在中斷後3s停止" );
|
29 |
System.err.println( "JMV bug" );
|
30 |
System.err.println( "主線程中檢測thinkerThread的中斷狀態:" + thinkerThread.isInterrupted());
|
這個例子也很簡單,thinkerThread一直檢查中斷狀態,主線程在啟動thinkerThread之後的某個時刻調用interrupt中
斷thinkerThread。在《The Java Language Specification Java SE 7 Edition》§17.4.4中我們能夠看到,如果線程1調用線程2的interrupt方法,那麼所有線程(包括線程2)都能通過Thread.isInterrupted方法來檢測到這個中斷狀態。這裏直接用hotspot VM的-server模式執行一下,結果如下圖:

thinkerThread沒能退出循環,沒看到主線程所置的中斷狀態。
後麵這個例子是hotpost VM的一個bug導致的,在最新的hotspot中應該已經被修複了(筆者未測試最新版)。其它VM如IBM
J9,JRockit,harmony等並沒有發現這樣的bug。說這是bug,是因為JLS中規定了main發出的中斷必須對
thinkerThread可見。但是,如第一個例子,則不是bug,因為JLS是允許這種行為的。當在第一個例子的循環中的i++後麵加上一句
Thread.yield()調用(該調用在規範中並沒有特殊內存語義),這我使用的這個版本的VM上,就看不到可見性問題了。這也說明,JVM的優化是無法預知的,允許可見性的地方不一定就真會出現或一直出現。
JLS允許未充分同步的代碼出現可見性問題,但是某個實際的JVM完全可以實現的比JLS上規定的更強,比如不允許可見性問題出現,那麼,在這樣的
JVM上就展現不出這樣的問題了。第一個例子這裏隻是運行在hotpost下,也許在其它JVM下同樣采用最優化的方式執行,可能並不會出現這裏的問題。
在我們編碼的時候,也許並不知道代碼會跑在什麼樣的係統上,不知道會采用什麼樣的JVM,為了使得寫出的代碼更健壯,我們隻能按照規範所規定的最低
保證去編碼,要避免這類問題,隻有保證代碼充分同步,避免數據爭用,而不應該依賴於某個具體JVM實現。即使是具體的某款JVM,不同的版本間也可能存在
著差異。
最後,這樣的例子啟發我們,測試代碼的時候應盡可能啟用各JVM的最佳優化模式。
擴展閱讀:
至此,我們已經了解到實際中多線程運行真的會出現這樣的場景。為什麼會出現可見性問題?有什麼解決方案?下麵鏈接中的內容為我們提供了專業的解答。
文章轉自 並發編程網-ifeve.com
最後更新:2017-05-22 20:04:10