首先,每一個線程都有本身的工做內存,除此以外還有一個cpu的主存,工做內存是主存的副本。線程工做的時候,不能直接操做主內存中的值,而是要將主存的值拷貝到本身的工做內存中;在修改變量是,會先在工做內存中修改,隨後刷新到主存中。java
注意: 何時線程須要將主存中的值拷貝到工做內存編程
假設有一個共享變量flag爲false,線程a修改成true後,本身的工做內存修改了,也刷新到了主存。這時候線程b對flag進行對應操做時,是不知道a修改了的,也稱a對b不可見。因此咱們須要一種機制,在主存的值修改後,及時地通知全部線程,保證它們均可以看到這個變化。ide
public class ReadWriteDemo { //對於flag並無加volatile public boolean flag = false; public void change() { flag = true; System.out.println("flag has changed:" + flag); } public static void main(String[] args) { ReadWriteDemo readWriteDemo = new ReadWriteDemo(); //建立一個線程,用來修改flag,如上面描述的a線程 new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(3000); readWriteDemo.change(); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); //主線程,如上面描述的b線程 while(!readWriteDemo.flag) { } System.out.println("flag:" + readWriteDemo.flag); } }
按照分析,沒有加volatile的話,主線程(b線程)是看不到子線程(a線程)修改了flag的值。也就是說,在主線程看來,在沒有特殊狀況下,flag 永遠爲false, while(!readWriteDemo.flag) {}
的判斷條件爲true,系統不會執行到System.out.println("flag:" + readWriteDemo.flag);
優化
爲了不偶然性,我讓程序跑了6分鐘。能夠看到,子線程確實修改了flag的值,主線程也和咱們預期同樣,看不到flag的變化,一直在死循環。若是給flag變量加一個volatile呢,預期結果是,子線程修改變量對主線程來講是可見的,主線程會退出循環。spa
能夠看到,都不到一分鐘,在子線程修改flag的值後,主線程隨即就退出循環,說明馬上感知到了flag變量的變化。線程
有趣的是什麼呢:若是ab兩個線程間隔時間不長,當b線程也延遲10s讀(不是上面的馬上讀),你會發現兩個線程之間的修改也是可見的,爲何呢,stakc overflow上有解答,執行該線程的cpu有空閒時,會去主存讀取如下共享變量來更新工做內存中的值。更有趣的是,在寫這篇文章的時候,cpu及內存是這樣的,反而能正常執行,可是能出現問題就能說明volatile的做用。code
首先要先講一下java內存模型,java的的內存模型規定了工做內存與主存之間交互的協議,定義了8中原子操做:排序
我上網查了下資料,也看了不一樣的博客,有講到volatile其實在底層就是加了一個lock的前綴指令。lock前綴的指令要幹什麼上面也有寫。若是對帶有volatile的變量進行寫操做會怎麼呢。JVM會像處理器發送一條lock前綴的指令,a線程就鎖定主存內的變量,修改後再刷新到主存。b線程一樣會鎖定主存內的變量,可是會發現主存內的變量和工做內存的值不同,就會從主存中讀取最新的值。從而保證了每一個線程都能對變量的改變可見。內存
在編程世界裏面,原子性是指不能分割的操做,一個操做要麼所有執行,要麼所有不執行,是執行的最小單元。rem
public class TestAutomic { volatile int num = 0; void add() { num++; } public static void main(String[] args) throws InterruptedException { TestAutomic testAutomic = new TestAutomic(); for (int i = 0; i < 1000; i++) { new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(10); testAutomic.add(); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); } //等待12秒,讓子線程所有執行完 Thread.sleep(12000); System.out.println(testAutomic.num); } }
預期現象:都說不能保證原子性了,因此,應該結果是不等於1000
不一樣電腦執行的結果不同,個人是886,可能大家的不是,可是都說明了volatile都沒法保證操做的原子性。
這要從num++操做開始講起,num++操做能夠分爲三步:
咱們知道線程的執行具備隨機性,假設a線程和b線程中的工做內存中都是num=0,a線程先搶了cpu的執行權,在工做內存進行了加1操做,還沒刷新到主存中;b線程這時候拿到了cpu的執行權,也加1;接着a線程刷新到主存num=1,而b線程刷新到主存,一樣是num=1,可是兩次操做後num應該等於2。
解決方案:
對於咱們寫的程序,cpu會根據如何讓程序更高效來對指令經行重排序,什麼意思呢
a = 2; b = new B(); c = 3; d = new D();
通過優化後,可能真實的指令順序是:
a = 2; c = 3; b = new B(); d = new D();
並非全部的指令都會重排序,重排序與否全是看能不能使得指令更高效,還有下面一種狀況。
a = 2; b = a;
這兩行代碼不管什麼狀況下都不會重排序,由於第二條指令是依賴第一條指令的,重排序是創建在排序後最終結果仍然保持不變的基礎上。下面將給出volatile防止重排序的例子:
public class TestReorder { private static int a = 0, b = 0, x = 0, y = 0; public static void main(String[] args) throws InterruptedException { while (true) { a = 0; b = 0; x = 0; y = 0; //a線程 new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(10); a = 1; x = b; } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); //b線程 new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(10); b = 1; y = a; } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); //主線程睡100ms,以保證子線程所有執行完 Thread.sleep(100); System.out.println("a=" + a + ";b=" + b + ";x=" + x + ";y=" + y); } } }
還記得上面說過兩個線程若是沉睡時間差很少,它們之間是可見
預期結果:
能夠發現除了上面預期的三種狀況,還出現了一種a = 1; b = 1; x = 0; y = 0的狀況,相信你們也知道了,這種狀況就是由於重排序形成的。要麼是a線程重排序先執行x = b;
再執行a = 1;
,要麼是b線程重排序先執行了y = a;
再執行了b = 1;
;要麼是兩個線程都重排序了。
若是private volatile static int a = 0, b = 0, x = 0, y = 0;
加了volatile關鍵字會怎麼樣呢?
爲了保證正確性,又持續跑了5分鐘,能夠發現,確實不會再出現x=0;y=0的狀況。
先來說講4個內存屏障的做用
內存屏障 | 做用 |
---|---|
StoreStore屏障 | 禁止上面的普通寫和下面的的volatile寫重排序 |
StoreLoad屏障 | 禁止上面的volatile寫和下面volatile讀/寫重排序 |
LoadLoad屏障 | 禁止下面的普通讀和上面的volatile讀重排序 |
LoadStore屏障 | 禁止下面的普通寫和上面的volatile讀重排序 |
可能看做用比較抽象,直接舉例子叭
S1; StoreStore; S2
,在S2及後續寫入操做以前,保證S1的寫入操做對其它線程可見。S; StoreLoad; L
,在L及後續讀/寫操做以前,保證S的寫入對其它線程可見。L1; LoadLoad; L2
,在L2及後續讀操做以前,保證L1讀取數據完畢。L; LoadStore; S
,在S及後續操做以前,保證L讀取數據完畢。那麼volatile是如何保證有序性的呢?
舉例,有個對volatile變量的寫S,有個對volatile變量的讀L,會怎麼樣呢。
S1; StoreStore; S ;StoreLoad L
這樣可以把S(對volatile變量保護在中間)防止重排序。L1; LoadLoad; L ; LoadStore S
,同樣把volatile變量保護的好好的。有關volatile的講解就到這裏了。