隨筆-67  評論-522  文章-0  trackbacks-0
              Java并發一直都是開發中比較難也比較有挑戰性的技術,對于很多新手來說是很容易掉進這個并發陷阱的,其中尤以共享變量最具代表性,其實關于講這個知識點網上也不少,但大象想講講自己對這個概念的理解。
              共享變量比較典型的就是指類的成員變量,在類中定義了很多方法對成員變量的使用,如果是單實例,當有多個線程同時來調用這些方法,方法又沒加控制,那么這些方法對成員變量的操作就會使得該成員變量的值變得不準確了。
              大象用一個最典型的i++例子來說明:
              public class Test {
                  private int i = 0;
                  private final CountDownLatch mainLatch = new CountDownLatch(1);

                  public void add(){
                      i++;
                  }

                  private class Work extends Thread{
                      private CountDownLatch threadLatch;

                      public Work(CountDownLatch latch){
                          threadLatch = latch;
                      }

                      @Override
                      public void run() {
                          try {
                              mainLatch.await();
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
                          for (int j = 0; j < 1000; j++) {
                              add();
                          }
                          threadLatch.countDown();
                      }
                  }

                  public static void main(String[] args) throws InterruptedException {
                      for(int k = 0; k < 10; k++){
                          Test test = new Test();
                          CountDownLatch threadLatch = new CountDownLatch(10);
                          for (int i = 0; i < 10; i++) {
                              test.new Work(threadLatch).start();
                          }
                          test.mainLatch.countDown();
                          threadLatch.await();
                          System.out.println(test.i);
                      }
                  }
              }
              java.util.concurrent.CountDownLatchJDK5.0提供的關于并發的一個新API,它的作用就像一個門閂或是閘門那樣。上面這段代碼一共執行10次,每次啟動10個線程同時執行。mainLatch.await()相當于門閂擋著線程,讓準備好的線程處于等待狀態,當所有的線程都準備好時再調用mainLatch.countDown()方法,打開門閂讓線程同時執行。我在這里用這個類的原因,是想讓我創建的10個線程都準備好后再一起并發執行,這樣才能很明顯的看出add方法里面的i++效果。如果不引入CountDownLatch,只執行test.new Work(threadLatch).start(),則獲得的結果可能看不出來線程競爭共享變量產生的錯誤情況。threadLatch這個CountDownLatch的作用是讓10個線程都執行完run方法的for循環后通知主線程的threadLatch.await()停止等待打印出當前i的值。
              這段代碼我加了-server參數運行了多次,每次結果都不一樣,我取了幾個比較明顯的結果。當然,你也可以多運行幾次看看效果。
              
              
              
              共享變量i沒做任何同步操作,當有多個線程都要讀取并修改它時,問題就產生了。正確的結果應該是10000,但是我們看到了,不是每次結果都是10000。我這段代碼最初的版本不是這樣的,因為現在的CPU哪怕是家用級PCCPU核心頻率都非常高,所以完全看不出效果,在和一個朋友的討論中,他給出了修改的建議,最后改為上面的代碼,在這里謝謝Sunny君。run方法中的循環次數越大,i的并發問題就越明顯,大家可以動手試下。對于上圖的運行結果,和硬件平臺有關,也和-server參數有關。
              有同學會有疑問了,既然共享變量沒加同步處理,那為什么還是會出現10000的結果呢?關于這點我想這可能是JVM優化的結果,對于JVM(HotSpot)大象還沒有很深入的研究,不敢隨便下結論,請知道的朋友幫忙解答一下。
              Java中,線程是怎么操作共享變量的呢?我們都知道,Java代碼在編譯后會變成字節碼,然后在JVM里面運行,而像實例域(i)這樣的變量是存儲在堆內存(Heap Memory)中的,堆內存是內存中的一塊區域。線程的執行其實說到底就是CPU的執行,當今的CPU(Intel)基本上都是多核的,因此多線程都是由多核CPU來處理,并且都有L1、L2L3CPU緩存,CPU為了提高處理速度,在執行的時候,會從內存中把數據讀到緩存后再操作,而每個線程執行add方法操作i++的過程是這樣的:
                  1、線程從堆內存中讀取i的值,將它復制到緩存中
                  2、在緩存中執行i++操作,并將結果賦給變量i
                  3、再用緩存中的值刷新堆內存中的變量i的值
              我上面寫的這三步并不是嚴格按照JVMCPU指令的步驟來的,但過程就是這么一回事,方便大家理解。通過上面這個過程我們可以看出問題了,如果有多個線程同時要修改i,那么都需要先讀取堆內存中的變量i值,然后把它復制到緩存后執行i++操作,再將結果寫回到堆內存的變量i中。這個執行的時間非常短,可能只有零點幾納秒(主要還是跟硬件平臺有關),但還是出現了錯誤。產生這種錯誤的原因是共享變量的可見性,線程1在讀取變量i的值的時候,線程2正在更新變量i的值,而線程1這時看不到線程2修改的值。這種現象就是常說的共享變量可見性。
              下圖是線程執行的抽象圖,也可以說是Java內存模型的抽象示意圖,可能不嚴謹,但大意是這樣的。
              

              現在選用開發框架一般都會選擇Spring,或是類似Spring這樣的東西,而代碼中經常用到的依賴注入的Bean如果沒做處理一般都會是單例模式。試想一下,按下面這個方式引用Service或其它類似的Bean,在UserService中又不小心用到了共享變量,同時沒有處理它的共享可見性,即同步,那將會產生意想不到的結果。不光Service是單例的,Spring MVC中的Controller也是單例的,所以編寫代碼的時候一定要注意共享變量的問題。 
              @Autowired
              private UserService userService;
              所以我們要盡可能的不使用共享變量,避開它,因為處理好共享變量可見性不是一個很簡單的問題。如果有非用不可的理由,請使用java.util.concurrent.atomic包下面的原子類來代替常用變量類型。比如用AtomicInteger代替int,AtomicLong代替long等等,具體可以參考API文檔。如果需求比這更復雜,那還得想其它解決辦法。
              以上是大象關于共享變量的一些淺薄見解,有什么不對的,還請各位指出來。
              本文為菠蘿大象原創,如要轉載請注明出處。http://www.aygfsteel.com/bolo
          posted on 2014-06-10 16:09 菠蘿大象 閱讀(11310) 評論(5)  編輯  收藏 所屬分類: Concurrency

          評論:
          # re: 淺談Java共享變量[未登錄] 2014-06-17 21:33 | Gospel
          大象可以試一下在i前面加上關鍵字volatile試一下  回復  更多評論
            
          # re: 淺談Java共享變量 2014-06-18 16:39 | 菠蘿大象
          @Gospel
          volatile的變量可不是什么情況都適用的呦,不要亂用呦。我下一篇正準備談談volatile  回復  更多評論
            
          # re: 淺談Java共享變量[未登錄] 2014-06-19 23:26 | Gospel
          @菠蘿大象
          是的 感覺在用多線程的時候就像在走鋼絲   回復  更多評論
            
          # re: 淺談Java共享變量 2015-01-25 22:46 | Yaya
          參考你的博客 收益頗多 不過有一點不是很清楚 你說的 多核CPU情況下的共享變量那個圖 不是很理解 CPU多核 的情況下 每個CPU都有自己的緩存Cache CPU將計算的數據交給Cache之后刷新給內存 如果是多核多線程 那么針對某一個核CPU的所有線程 是共享變量 那么其他線程 結果可能會不如預期 這個顯然和我們所接受的共享變量不太一樣 不清楚這個地方 你是如何證明的(剛入門Java一年 想法有點幼稚 莫怪)   回復  更多評論
            
          # re: 淺談Java共享變量 2015-01-26 09:04 | 菠蘿大象
          @Yaya
          你可以看看我另一篇文章"淺談volatile變量的理解"  回復  更多評論
            
          主站蜘蛛池模板: 吴忠市| 环江| 阿巴嘎旗| 永宁县| 太湖县| 长汀县| 紫阳县| 德保县| 和林格尔县| 句容市| 石城县| 思茅市| 定陶县| 连平县| 宕昌县| 烟台市| 剑川县| 金坛市| 泰安市| 大埔县| 灵山县| 那曲县| 景东| 大荔县| 祁连县| 江达县| 昆明市| 临颍县| 宿松县| 吴旗县| 洪雅县| 瑞安市| 聊城市| 肇东市| 柯坪县| 陆川县| 昌吉市| 阿鲁科尔沁旗| 阳新县| 巧家县| 连云港市|