併發編程(二):全視角解析volatile

1、目錄

  一、引入話題-發散思考html

  二、volatile深度解析java

  三、解決volatile原子性問題編程

  四、volatile應用場景緩存

2、引入話題-發散思考

public class T1 { /*volatile*/ boolean running=true; public void m(){ System.out.println(Thread.currentThread().getName()+":start!"); while(running){ /*try { TimeUnit.MINUTES.sleep(2); } catch (Exception e) { e.printStackTrace(); }*/ } System.out.println(Thread.currentThread().getName()+":end!"); } public static void main(String[] args) { T1 t=new T1(); new Thread(()->t.m(),"t").start(); try { TimeUnit.SECONDS.sleep(1); } catch (Exception e) { e.printStackTrace(); } t.running=false; } } 運行結果: 無volatile: t:start! 有volatile: t:start! t:end!
含有volatile是指望的結果,那爲何不添加volatile會產生這種狀況呢?
再談Java內存模型:
     在虛擬機中,堆內存用於存儲共享數據(實例對象),堆內存也就是這裏說的主內存。
     每一個線程將會在堆內存中開闢一塊空間叫作線程的工做內存,附帶一塊緩存區用於存儲共享數據副本。那麼,共享數據在堆內存當中,線程通訊就是經過主內存爲中介,線程在本地內存讀而且操做完共享變量操做完畢之後,把值寫入主內存。
 
解析無volatile:
  • 根據上述java內存模型可知,最開始running=true在主存中,開啓線程A,線程會把主存的running=true複製一份寫入工做內存的共享變量副本中。
  • 當咱們改變running=false,在主存中已經發生改變。
  • 線程A一直在工做狀態,沒有空閒時間去知道主存的狀況,而是一直在讀本地內存的共享變量副本,也就一直running=true,取而代之也會產生上述狀況。

3、volatile深度解析

那爲何含有volatile就能及時刷新工做內存呢?它有什麼做用呢?
volatile:可見性(一個線程修改共享變量之後,立馬會被其餘線程知可見)、禁止重排序。
 
一、什麼是可見性?
虛擬機的happens-before中的volatile規則:volatile變量寫操做先於讀操做,一個線程去讀取volatile變量,另外一個線程去寫volatile變量,那麼volatile變量的寫操做優先。
  • 根據上述java內存模型可知,最開始running=true在主存中,開啓線程A,線程會把主存的running=true複製一份寫入工做內存的共享變量副本中。
  • 當咱們改變running=false,在主存已經發生改變。
  • 就在這時,當主存與工做內存發生不一致的時候,工做內存的共享變量會失效,那麼工做內存就會去主存刷新一遍共享變量,因此running=false,天然就執行下面的代碼啦!

 

二、什麼是禁止重排序?
先談有序性:
int a=1; int b =3; int c=a*b;
在虛擬機中,執行上述代碼,必定是按照上述順序執行嗎?那可不必定,像a=1,b=3的順序徹底可能先執行b=3,a=1,這被稱爲重排序;可是c=a*b必定在a=1,b=3後面,這被成爲有序性。
 
再談重排序:
//線程1:
context = loadContext();  //語句1
inited = true;            //語句2 //線程2:
while(!inited ){ sleep() } doSomethingwithconfig(context);
假設上述代碼在單線程中,談太重排序不會對代碼形成什麼影響,可是咱們看這一段代碼。
語句1與語句2並無太多的依賴關係,參考有序性例子,那麼他們就能夠重排序,那麼可能語句2執行先於語句1,假設在線程1中語句2執行完就刷新inited到主存,還沒等語句1執行呢?線程2就執行起來,一看inited=true,挑出循環,執行下面的代碼,context=null就報出空指針,顯然這是不能被虛擬機容許的。
 
因此,volatile明確規定禁止重排序,意思就是context=loadContext必須先於inited執行。
在虛擬機中設定了有序性,也就是前面談到的happens-before原則。若是不知足上該原則的狀況下,虛擬機是能夠自由的重排序的,下面附錄此規則。
 
三、先行發生原則(happens-before)
    • 程序次序規則:一個線程內,按照代碼順序,書寫在前面的操做先行發生於書寫在後面的操做
    • 鎖定規則:一個unLock操做先行發生於後面對同一個鎖的lock操做
    • volatile變量規則:對一個變量的寫操做先行發生於後面對這個變量的讀操做
    • 傳遞規則:若是操做A先行發生於操做B,而操做B又先行發生於操做C,則能夠得出操做A先行發生於操做C
    • 線程啓動規則:Thread對象的start()方法先行發生於此線程的每一個一個動做
    • 線程中斷規則:對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生
    • 線程終結規則:線程中全部的操做都先行發生於線程的終止檢測,咱們能夠經過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執行
    • 對象終結規則:一個對象的初始化完成先行發生於他的finalize()方法的開始

4、解決volatile原子性問題

一、volatile能解決原子性問題嗎?什麼是原子性呢,本不想解釋,爲了讀者可以更透徹理解,再解釋一下。併發

原子性:只有一個線程訪問共享數據,也就是當線程A訪問一個代碼塊的時候,其餘線程所有堵塞,只有等代碼塊所有執行完,才能被其餘線程訪問共享數據。
 
public class T2 { volatile int count=0; public void m(){ for(int i=0;i<1000;i++) count++; } public static void main(String[] args) { T2 t=new T2(); List<Thread> threads=new ArrayList<Thread>(); for(int i=0;i<10;i++){ threads.add(new Thread(()->t.m(),"thread-"+i)); } threads.forEach((o)->o.start()); //等待全部線程都執行完
           threads.forEach((o)->o.yield()); System.out.println("count:"+t.count); } } 運行結果: count:8710 //每次都不同。

 

 二、爲何加了volatile仍是不能獲得預期結果呢?由於它只保證了可見性,不能保證原子性。what?app

再回憶java內存模型:jvm

 

三、那怎麼解決呢?優化

方式一:synchronized,jvm對synchronized進行了很大的優化,因此效率也沒有想象中那麼低。spa

public class T3 { int count=0; public synchronized void m(){ for(int i=0;i<1000;i++) count++; } public static void main(String[] args) { T3 t=new T3(); List<Thread> threads=new ArrayList<Thread>(); for(int i=0;i<10;i++){ threads.add(new Thread(()->t.m(),"thread-"+i)); } threads.forEach((o)->o.start()); //等待全部線程都執行完
           threads.forEach((o)->o.yield()); System.out.println("count:"+t.count); } }

方式二:ReentrantLock,跟synchronized的做用差很少。線程

public class T5 { ReentrantLock lock=new ReentrantLock(); int  count=0; public void m(){ lock.lock(); for(int i=0;i<1000;i++) count++; lock.unlock(); } public static void main(String[] args) { T4 t=new T4(); List<Thread> threads=new ArrayList<Thread>(); for(int i=0;i<10;i++){ threads.add(new Thread(()->t.m(),"thread-"+i)); } threads.forEach((o)->o.start()); //等待全部線程都執行完
           threads.forEach((o)->o.yield()); System.out.println("count:"+t.count); } }
方式三:AtomicInteger原子類
public class T4 { AtomicInteger count=new AtomicInteger(0); public void m(){ for(int i=0;i<1000;i++) count.getAndIncrement(); } public static void main(String[] args) { T4 t=new T4(); List<Thread> threads=new ArrayList<Thread>(); for(int i=0;i<10;i++){ threads.add(new Thread(()->t.m(),"thread-"+i)); } threads.forEach((o)->o.start()); //等待全部線程都執行完
           threads.forEach((o)->o.yield()); System.out.println("count:"+t.count); } }

5、volatile應用場景

說到這裏,讀者可能就已經懵逼了,這也有問題那也有問題,那咱們何時用它呢?
volatile是基於synchronized提出的效率優化手段,可是它是不能代替synchronized的。
 
狀態標記量:
通常volatile共享變量,不要用於數據計算,最好去標記一些狀態值,好比前面說的running=true。
volatile boolean inited = false; //線程1:
context = loadContext(); inited = true; //線程2:
while(!inited ){ sleep() } doSomethingwithconfig(context);

 9、版權聲明

  做者:邱勇Aaron

  出處:http://www.cnblogs.com/qiuyong/

  您的支持是對博主深刻思考總結的最大鼓勵。

  本文版權歸做者全部,歡迎轉載,但未經做者贊成必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接,尊重做者的勞動成果。

  參考:深刻理解JVM、馬士兵併發編程、併發編程實踐

     volatile關鍵字解析:http://www.importnew.com/18126.html

相關文章
相關標籤/搜索