1. 程式人生 > >轉: 【Java並發編程】之十八:第五篇中volatile意外問題的正確分析解答(含代碼)

轉: 【Java並發編程】之十八:第五篇中volatile意外問題的正確分析解答(含代碼)

深入 規則 rup lis con method 執行 change .text

轉載請註明出處:http://blog.csdn.net/ns_code/article/details/17382679


《Java並發編程學習筆記之五:volatile變量修飾符—意料之外的問題》一文中遺留了一個問題,就是volatile只修飾了missedIt變量,而沒修飾value變量,但是在線程讀取value的值的時候,也讀到的是最新的數據。但是在網上查了很多資料都無果,看來很多人對volatile的規則並不是太清晰,或者說只停留在很表面的層次,一知半解。

這兩天看《深入Java虛擬機——JVM高級特性與最佳實踐》第12章:Java內存模型與線程,並在網上查閱了Java內存模型相關資料,學到了不少東西,尤其在看這篇文章的volatile部分的講解之後,算是確定了問題出現的原因。

首先明確一點:假如有兩個線程分別讀寫volatile變量時,線程A寫入了某volatile變量,線程B在讀取該volatile變量時,便能看到線程A對該volatile變量的寫入操作,關鍵在這裏,它不僅會看到對該volatile變量的寫入操作,A線程在寫volatile變量之前所有可見的共享變量,在B線程讀同一個volatile變量後,都將立即變得對B線程可見。

回過頭來看文章中出現的問題,由於程序中volatile變量missedIt的寫入操作在value變量寫入操作之後,而且根據volatile規則,又不能重排序,因此,在線程B讀取由線程A改變後的missedIt之後,它之前的value變量在線程A的改變也對線程B變得可見了。

我們顛倒一下value=50和missedIt=true這兩行代碼試下,即missedIt=true在前,value=50在後,這樣便會得到我們想要的結果:value值的改變不會被看到。

這應該是JDK1.2之後對volatile規則做了一些修訂的結果。


修改後的代碼如下:

[java] view plain copy
  1. public class Volatile extends Object implements Runnable {
  2. //value變量沒有被標記為volatile
  3. private int value;
  4. //missedIt變量被標記為volatile
  5. private volatile boolean missedIt;
  6. //creationTime不需要聲明為volatile,因為代碼執行中它沒有發生變化
  7. private long creationTime;
  8. public Volatile() {
  9. value = 10;
  10. missedIt = false;
  11. //獲取當前時間,亦即調用Volatile構造函數時的時間
  12. creationTime = System.currentTimeMillis();
  13. }
  14. public void run() {
  15. print("entering run()");
  16. //循環檢查value的值是否不同
  17. while ( value < 20 ) {
  18. //如果missedIt的值被修改為true,則通過break退出循環
  19. if ( missedIt ) {
  20. //進入同步代碼塊前,將value的值賦給currValue
  21. int currValue = value;
  22. //在一個任意對象上執行同步語句,目的是為了讓該線程在進入和離開同步代碼塊時,
  23. //將該線程中的所有變量的私有拷貝與共享內存中的原始值進行比較,
  24. //從而發現沒有用volatile標記的變量所發生的變化
  25. Object lock = new Object();
  26. synchronized ( lock ) {
  27. //不做任何事
  28. }
  29. //離開同步代碼塊後,將此時value的值賦給valueAfterSync
  30. int valueAfterSync = value;
  31. print("in run() - see value=" + currValue +", but rumor has it that it changed!");
  32. print("in run() - valueAfterSync=" + valueAfterSync);
  33. break;
  34. }
  35. }
  36. print("leaving run()");
  37. }
  38. public void workMethod() throws InterruptedException {
  39. print("entering workMethod()");
  40. print("in workMethod() - about to sleep for 2 seconds");
  41. Thread.sleep(2000);
  42. //僅在此改變value的值
  43. missedIt = true;
  44. // value = 50;
  45. print("in workMethod() - just set value=" + value);
  46. print("in workMethod() - about to sleep for 5 seconds");
  47. Thread.sleep(5000);
  48. //僅在此改變missedIt的值
  49. // missedIt = true;
  50. value = 50;
  51. print("in workMethod() - just set missedIt=" + missedIt);
  52. print("in workMethod() - about to sleep for 3 seconds");
  53. Thread.sleep(3000);
  54. print("leaving workMethod()");
  55. }
  56. /*
  57. *該方法的功能是在要打印的msg信息前打印出程序執行到此所化去的時間,以及打印msg的代碼所在的線程
  58. */
  59. private void print(String msg) {
  60. //使用java.text包的功能,可以簡化這個方法,但是這裏沒有利用這一點
  61. long interval = System.currentTimeMillis() - creationTime;
  62. String tmpStr = " " + ( interval / 1000.0 ) + "000";
  63. int pos = tmpStr.indexOf(".");
  64. String secStr = tmpStr.substring(pos - 2, pos + 4);
  65. String nameStr = " " + Thread.currentThread().getName();
  66. nameStr = nameStr.substring(nameStr.length() - 8, nameStr.length());
  67. System.out.println(secStr + " " + nameStr + ": " + msg);
  68. }
  69. public static void main(String[] args) {
  70. try {
  71. //通過該構造函數可以獲取實時時鐘的當前時間
  72. Volatile vol = new Volatile();
  73. //稍停100ms,以讓實時時鐘稍稍超前獲取時間,使print()中創建的消息打印的時間值大於0
  74. Thread.sleep(100);
  75. Thread t = new Thread(vol);
  76. t.start();
  77. //休眠100ms,讓剛剛啟動的線程有時間運行
  78. Thread.sleep(100);
  79. //workMethod方法在main線程中運行
  80. vol.workMethod();
  81. } catch ( InterruptedException x ) {
  82. System.err.println("one of the sleeps was interrupted");
  83. }
  84. }
  85. }

執行結果如下:

技術分享


很明顯,這其實並不符合使用volatile的第二個條件:附上一篇講述volatile關鍵字正確使用的很好的文章:http://www.ibm.com/developerworks/cn/java/j-jtp06197.html



轉: 【Java並發編程】之十八:第五篇中volatile意外問題的正確分析解答(含代碼)