您好,登錄后才能下訂單哦!
本篇內容主要講解“什么是volatile”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“什么是volatile”吧!
保證了各個線程之間的可見性
不能保證原子性
防止重排序
首先,每個線程都有自己的工作內存,除此之外還有一個cpu的主存,工作內存是主存的副本。線程工作的時候,不能直接操作主內存中的值,而是要將主存的值拷貝到自己的工作內存中;在修改變量是,會先在工作內存中修改,隨后刷新到主存中。
注意: 什么時候線程需要將主存中的值拷貝到工作內存
線程中釋放鎖的時
線程切換時
CPU有空閑時間時(比如線程休眠時)
假設有一個共享變量flag為false,線程a修改為true后,自己的工作內存修改了,也刷新到了主存。這時候線程b對flag進行對應操作時,是不知道a修改了的,也稱a對b不可見。所以我們需要一種機制,在主存的值修改后,及時地通知所有線程,保證它們都可以看到這個變化。
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呢,預期結果是,子線程修改變量對主線程來說是可見的,主線程會退出循環。
可以看到,都不到一分鐘,在子線程修改flag的值后,主線程隨即就退出循環,說明立刻感知到了flag變量的變化。
有趣的是什么呢:如果ab兩個線程間隔時間不長,當b線程也延遲10s讀(不是上面的立刻讀),你會發現兩個線程之間的修改也是可見的,為什么呢,stakc overflow上有解答,執行該線程的cpu有空閑時,會去主存讀取以下共享變量來更新工作內存中的值。更有趣的是,在寫這篇文章的時候,cpu及內存是這樣的,反而能正常執行,但是能出現問題就能說明volatile的作用。
首先要先講一下java內存模型,java的的內存模型規定了工作內存與主存之間交互的協議,定義了8中原子操作:
lock:將主內存的變量鎖定,為一個線程所獨占。
unlock:將lock加的鎖定解除,此時其他線程可以有機會訪問此變量。
read:將主內存中的變量值讀到工作線程中。
load:將read讀取到的值保存到工作內存中的變量副本中。
use:將值傳遞給線程的代碼執行引擎。
assign:將執行引擎處理返回的值重新賦值給變量副本。
store:將變量副本的值存儲到主內存中。
write:將store存儲的值寫入到主內存的共享變量中。
我上網查了下資料,也看了不同的博客,有講到volatile其實在底層就是加了一個lock的前綴指令。lock前綴的指令要干什么上面也有寫。如果對帶有volatile的變量進行寫操作會怎么呢。JVM會像處理器發送一條lock前綴的指令,a線程就鎖定主存內的變量,修改后再刷新到主存。b線程同樣會鎖定主存內的變量,但是會發現主存內的變量和工作內存的值不一樣,就會從主存中讀取最新的值。從而保證了每個線程都能對變量的改變可見。
在編程世界里面,原子性是指不能分割的操作,一個操作要么全部執行,要么全部不執行,是執行的最小單元。
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++操作可以分為三步:
讀取i的值,裝載進工作內存
對i加1操作
將i的值寫回工作內存,刷新到主存中
我們知道線程的執行具有隨機性,假設a線程和b線程中的工作內存中都是num=0,a線程先搶了cpu的執行權,在工作內存進行了加1操作,還沒刷新到主存中;b線程這時候拿到了cpu的執行權,也加1;接著a線程刷新到主存num=1,而b線程刷新到主存,同樣是num=1,但是兩次操作后num應該等于2。
解決方案:
使用synchronized關鍵字
使用原子類
對于我們寫的程序,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線程(a = 1, x = b = 0),再執行完b線程(b = 1, y = a = 1),最終結果a = 1; b = 1; x = 0; y = 1
如果先執行完b線程(b = 1, y = a = 0),再執行完a線程(a = 1, x = b = 1),最終結果a = 1; b = 1; x = 1; y = 0
如果執行a線程過程(a = 1),接著執行了b線程(b = 1,y = a = 1)【為什么y = a一定等于1,因為它們兩個之間的改變是可見的】,最后執行了a線程(x = b = 1),最終結果a = 1;b = 1; x = 1; y = 1
可以發現除了上面預期的三種情況,還出現了一種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寫操作前插入StoreStore屏障,每個寫操作后面加一個StoreLoad屏障。
在每個volatile讀操作前插入LoadLoad屏障,在讀操作后插入LoadStore屏障。
舉例,有個對volatile變量的寫S,有個對volatile變量的讀L,會怎么樣呢。
對于寫:S1; StoreStore; S ;StoreLoad L
這樣能夠把S(對volatile變量保護在中間)防止重排序。
對于讀一樣的道理:L1; LoadLoad; L ; LoadStore S
,一樣把volatile變量保護的好好的。
到此,相信大家對“什么是volatile”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。