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關鍵字我們就學習到這裡。