最后一面掛在volatile關鍵字上,面試官:重新學學Java吧!
為什么會有volatile關鍵字?
volatile: 易變的; 無定性的; 無常性的; 可能急劇波動的; 不穩定的; 易惡化的; 易揮發的; 易發散的;
從上面的單詞本意我們可以知道這個關鍵詞用于修飾那些易變的變數
為了讓我們更好理解為什么volatile這個關鍵字的作用以及存在的意義
我們先來看一段代碼:
package com.laoqin.juc;
/**
* @Description TODO 測驗volatile關鍵字
* @author LaoQin
*/
public class TestVolatile {
public static void main(String[] args) {
ThreadDemo td = new ThreadDemo();
new Thread(td).start();
while (true){
if(td.isFlag()){
System.out.println("主執行緒獲取到flag為true");
break;
}
}
}
}
class ThreadDemo implements Runnable{
private boolean flag = false;
@Override
public void run() {
/*睡眠2秒*/
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("flag="+ isFlag());
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
這段代碼想表述的邏輯很簡單,內部類實作了Runnable介面,run方法內部對flag進行了簡單的賦值操作,并在主執行緒中寫了一個死回圈去不斷判斷flag的值,只要為true那么這個程式就會結束運行
但是事實也是如此嗎?
我們等待了很久依然等不到程式結束,這是為什么呢?
我們得到的結果如下
flag=true
說明在執行緒內部我們的flag值確實是true,但是在主執行緒中的flag卻一直是false,這就造成了我們的回圈成為一個真正的"死回圈"
這就涉及到一個"記憶體可見性"的問題了
記憶體可見性
JVM為了提升程式的運行效率,會為我們程式當中每一個執行緒分配一個獨立的"快取空間",這個"快取空間"對應著jvm調優引數中的 -Xss512k ,這表示為每個執行緒分配的"快取空間"為512kb
程式運行程序中首先會有一個主存,拿上面的例子來說
主存中 flag = false;
然后啟動兩個執行緒,一個是讀(主執行緒),一個是寫(子執行緒)
因為子執行緒中休眠了2秒,所以是主執行緒先執行
因為子執行緒要改變資料,所以子執行緒是先把flag=false這條資料讀到自己的"快取"中來
然后在自己的記憶體空間先改變這個副本,然后再把這個值寫回到主存中去
資料的運算都是在快取中執行
主執行緒在子執行緒還沒有改變主存值的時候就已經讀取了false到自己的快取
因為主執行緒呼叫的是while(true),JVM會呼叫系統底層代碼,執行效率很高
甚至高到主執行緒沒有機會再去主存中獲取資料
這就是一個典型的記憶體可見性問題:
即兩個執行緒在共享同一資料的時,共享資料的所有操作對于每個獨立記憶體來說都是不可見的
對于以上的問題,我們可以通過同步鎖來解決
package com.laoqin.juc;
/**
* @Description TODO 測驗volatile關鍵字
* @author LaoQin
*/
public class TestVolatile {
public static void main(String[] args) {
ThreadDemo td = new ThreadDemo();
new Thread(td).start();
while (true){
//改動了這里
synchronized (td){
if(td.isFlag()){
System.out.println("主執行緒獲取到flag為true");
break;
}
}
}
}
}
class ThreadDemo implements Runnable{
private boolean flag = false;
@Override
public void run() {
/*睡眠2秒*/
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("flag="+ isFlag());
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
這樣通過給物件加鎖,就可以解決這個問題,即保持多個執行緒之間資料的同步(鎖住共享資料或者共享資料所在物件)
得到以下效果
主執行緒獲取到flag為true
flag=true
但是加鎖意味著我們程式的效率將會變得極其低下
當有多個執行緒同時訪問的時候,后來的執行緒必須要等待前面的執行緒釋放被鎖資源才能進行操作
這就是volatile存在的意義
volatile用法
volatile能保證多個執行緒在操作同一個資料時,這個資料對于所有執行緒來說是可見的
底層是因為volatile會讓jvm去呼叫計算機底層的"記憶體柵欄"
記憶體屏障,也稱記憶體柵欄,記憶體柵障,屏障指令等, 是一類同步屏障指令,是CPU或編譯器在對記憶體隨機訪問的操作中的一個同步點,使得此點之前的所有讀寫操作都執行后才可以開始執行此點之后的操作,
語意上,記憶體屏障之前的所有寫操作都要寫入記憶體;記憶體屏障之后的讀操作都可以獲得同步屏障之前的寫操作的結果,因此,對于敏感的程式塊,寫操作之后、讀操作之前可以插入記憶體屏障,
我們可以簡單理解為volatile能讓所有執行緒的操作都在主存中完成,這樣就規避了記憶體可見性的問題
這樣會比鎖的效率高很多,但是還是會比不加該關鍵字運行效率低不少
原因是JVM底層優化邏輯中對violatile修飾的變數會進行重排序,這個會比較耗時
以下是使用volatile的代碼
package com.laoqin.juc;
/**
* @Description TODO 測驗volatile關鍵字
* @author LaoQin
*/
public class TestVolatile {
public static void main(String[] args) {
ThreadDemo td = new ThreadDemo();
new Thread(td).start();
while (true){
if(td.isFlag()){
System.out.println("主執行緒獲取到flag為true");
break;
}
}
}
}
class ThreadDemo implements Runnable{
//改動了這里
private volatile boolean flag = false;
@Override
public void run() {
/*睡眠2秒*/
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("flag="+ isFlag());
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
得到的結果和加鎖的一樣
主執行緒獲取到flag為true
flag=true
volatile和synchronized異同
相較于synchronized,volatile是一種輕量級的"資料同步策略"
但是volatile不具備"互斥性",即synchronized修飾的資料一旦上鎖后別的執行緒是無法操作該資料的,但volatile修飾的變數只是會讓該資料在主存中完成操作,并不會讓資料具有"互斥性"
同時volatile也不能保證變數的"原子性",即volatile不能保證變數是一個不可分割的整體
相信通過這篇文章簡短的敘述,各位也對volatile有了一定基礎的認識,更多高級的技巧方丈建議看官可以取閱讀一下JDK原始碼,看看Oracle Java小組的大神們都是如何將這個關鍵字用得出神入化的!
方丈全堆疊?著作權所有,轉載請注明出處,如有盜用,后果自負!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/163259.html
標籤:Java
上一篇:Java中trim方法
