學習互連網架構第四課(volatile關鍵字)

來源:互聯網
上載者:User

       volatile概念:volatile關鍵字的主要作用是使變數在多個線程間可見。

       在說volatile關鍵字之前,先來看兩個小例子

package com.internet.thread;public class RunThread extends Thread{    private int num = 0;    public void setNum(int num){    System.out.println(this.num);    this.num = num;    }    public void run(){    System.out.println(num);    }    public static void main(String[] args){        RunThread t1 = new RunThread();    t1.setNum(10);    t1.start();    try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}        RunThread t2 = new RunThread();        t2.setNum(20);        t2.start();    }        }
      運行結果如下,可以看到,兩個線程操作的num完全沒有關係,各自操作各自的。

010020
       假如我們在num前面加上static修飾

private static int num = 0;
       下面再運行main方法,結果如下,說明這時兩個線程操作的是同一個變數num。

0101020
        但是多個線程同時訪問同一個變數a的時候,就會出現線程問題,如下圖所示。針對線程問題,我們可以採取給變數a加synchronized鎖,這樣無論多少個線程訪問變數a都要一個一個的來,其中一個線程操作a的期間其它線程不能操作變數a,但是這樣有個很大的問題就是並發太低。


         

             下面我們再來看個例子,代碼如下

package com.internet.thread;public class VolatileThread extends Thread{    private boolean isRunning  = true;    private void setRunning(boolean isRunning){    this.isRunning = isRunning;    }        public void run(){    System.out.println("進入run方法..");    while(isRunning == true){    //..    }    System.out.println("線程停止");    }        public static void main(String[] args) throws InterruptedException{    VolatileThread vt = new VolatileThread();    vt.start();    Thread.sleep(3000);    vt.setRunning(false);    System.out.println("isRunning的值已經被設定了false");    Thread.sleep(1000);    System.out.println(vt.isRunning);    }}
           運行結果如下圖所示,可以看到,雖然isRunning變數的值變成了false,但是while迴圈依然在執行,如下圖所示。這顯然不合理的。

        那麼,為什麼我們把變數值isRunning變成false而while迴圈卻不停止呢。這其實是JDK的設計造成的,如下圖所示,JDK在設計線程的時候引入了線程工作記憶體的機制,變數在主記憶體中有一份isRunning變數,線上程工作記憶體中存了該變數的一個副本,線程在執行的時候判斷isRunning變數值的時候是從線程工作記憶體中去擷取的,當我們在主線程中設定isRunning的值為false時,主記憶體中的isRunning變數的值已經變成false了,但是線程工作記憶體中的isRunning副本的值還是true,因此我們才會看到while迴圈還在一直啟動並執行原因。JDK這樣做的目的是為了避免每次擷取變數值都要去主記憶體擷取,因為這樣比較消耗效能。


        那麼,我們應該怎樣解決這個問題呢。其實方案很簡單,就是給isRunning加上volatile關鍵字修飾,然後重新運行main方法,這次發現while迴圈結束了。這才是正常的運行結果。

          這時工作機制如下圖所示。可以看到,當變數被volatile關鍵字修飾後,線程執行引擎就會去主記憶體中去讀取變數值,同時主記憶體會把改變的變數值更新到線程工作記憶體當中。


         用volatile關鍵字修飾變數雖然可以讓變數在多個線程間可見,但是它並不具有原子性,我們來看下面一個例子,定義了一個addCount方法,調用一次count就加1000,如果count具有原子性的話,最後的結果應該是10000。

package com.internet.thread;public class VolatileNoAtomic extends Thread{    private static volatile int count;    private static void addCount(){    for(int i=0;i<1000;i++){    count++;    }    System.out.println(count);    }        public void run(){    addCount();    }        public static void main(String[] args){    VolatileNoAtomic[] arr = new VolatileNoAtomic[10];    for(int i=0;i<10;i++){    arr[i] = new VolatileNoAtomic();    }    for(int i=0;i<10;i++){    arr[i].start();    }    }}
          我們運行上面的代碼,結果如下,可以看到最後的結果是8839,並不是我們期望的10000,從而可以得出結論:用volatile關鍵字修飾的變數並不具有原子性。

2000400030002000500062406763683978398839
        那麼,怎樣才能讓變數count具有原子性呢。我們可以使用AtomicInteger,如下圖所示。


        修改後,我們再運行下main方法,結果如下,雖然中間的過程不具有原子性,但是最終的結果一定是具有原子性的,這樣做的好處是多個線程可以同時執行,中間過程可能有短暫的資料不一致,但是最終的結果一定是正確的。這樣的例子也很常見,比如我們雙11搶購商品,這麼大的並發量,要說一下子就把所有資料都準確的統計出來是不可能的,因為並發量太大了,根本來不及統計,於是退而求其次,允許短暫的資料不一致,但是最終一定要做到資料準確、一致。

10002000416550004724629670008903900010000
        volatile關鍵字雖然擁有多個線程之間的可見度,但是卻不具備同步性(也就是原子性),可以算上是一個輕量級的synchronized,效能要比synchronized強很多,不會造成阻塞(在很多開源的架構裡,比如netty的底層代碼就大量使用volatile,可見netty效能一定是非常不錯的。)這裡需要注意:一般volatile用於只針對於多個線程可見的變數操作,並不能代替synchronized的同步功能。實現原子性建議使用atomic類的系列對象,支援原子性操作(注意atomic類只保證本身方法原子性,並不保證多次操作的原子性)

       下面我們便來舉個例子來說明atomic類不保證多次操作原子性,代碼如下(注意此時multiAdd方法前是沒有synchronized修飾的)

package com.internet.thread;import java.util.ArrayList;import java.util.List;import java.util.concurrent.atomic.AtomicInteger;public class AtomicUse {    private static AtomicInteger count = new AtomicInteger(0);    //多個addAndGet在一個方法內是非原子性的,需要加synchronized進行修飾,保證4個    //addAndGet整體原子性    public  int multiAdd(){    try {Thread.sleep(100);} catch (Exception e) {e.printStackTrace();}    count.addAndGet(1);    count.addAndGet(2);    count.addAndGet(3);    count.addAndGet(4);//1+2+3+4=10,也就是說,執行一次multiAdd方法,count就加10    return count.get();    }        public static void main(String[] args){    final AtomicUse au = new AtomicUse();    List<Thread> ts = new ArrayList<Thread>();    for(int i=0;i<100;i++){    ts.add(new Thread(new Runnable() {@Overridepublic void run() {System.out.println(au.multiAdd());}}));    }    for(Thread t:ts){    t.start();    }    }}
        我們運行main方法,結果如下所示,如果multiAdd具有原子性的話,那麼應該是整10的增加,但是我們看到中間出現了諸如223、231這樣的數字,說明atomic類確實不能保證多次操作的原子性(如果唯寫一個addAndGet方法的話,是支援原子性的,現在是4個,因此不支援方法的原子性了)。不過,雖然不能保證multiAdd方法的原子性,但是最終的結果是正確的,那就是1000,無論運行多少次,一定有1000,這說明最終是正確的。

1020304060607090801001101301201401501601701802002102002232502312402603002902802703103403303213503603803703904004104304204404504605205105004964704805305405515605705966106306065926506406206707006906806707407807607707507307307318008108008308308708708708708908909109009509509509509601000970990980
       如果我們要保證multiAdd方法的原子性的話,我們就給multiAdd方法添加synchronized關鍵字,如下圖所示。


          我們再運行main方法,運行結果如下(由於運行結果太長,我只截取了最後面一段),可以看到數字count確實是整10的增加的,直到1000。

8308408508608708808909009109209309409509609709809901000
         volatile關鍵字我們就學習到這裡。
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.