volatile關鍵字16問
1.volatile 關鍵字是什麼意思?
volatile的作用是作為指令關鍵字,確保本條指令不會因編譯器的優化而省略,且要求每次直接讀值。volatile的變數是說這變數可能會被意想不到地改變,這樣,編譯器就不會去假設這個變數的值了。
2.你瞭解到的什麼地方使用了 volatile 關鍵字?解決了什麼問題?
在juc包下的大量的類都使用到了volatile關鍵字,volatile關鍵字解決執行緒變數的可見性問題。
3.volatile 和 JMM 有什麼關係?
每個執行緒讀取和儲存的都是執行緒的工作記憶體。而執行緒的工作記憶體再到主存中的儲存是肯定會有一些時差的。也就是改變了一個變數的值之後,另一個呼叫這個變數的物件是不能馬上知道的。如果說要讓其他執行緒立即可見這個改動,就要使用volatile關鍵字修飾。一旦使用這個關鍵字之後,所有呼叫這個變數的執行緒就直接去主存當中拿取資料。每個執行緒不能訪問其他執行緒的工作記憶體。
4.什麼是指令重排?volatile 和指令重排有什麼關係?
指令重排:指令重排序是JVM為了優化指令,提高程式執行效率,在不影響單執行緒程式執行結果的前提下,儘可能地提高並行度。編譯器、處理器也遵循這樣一個目標。注意是單執行緒。多執行緒的情況下指令重排序就會給程式設計師帶來問題。
在JDK1.5之後,可以使用volatile變數禁止指令重排序。volatile關鍵字通過提供“記憶體屏障”的方式來防止指令被重排序,為了實現volatile的記憶體語義,編譯器在生成位元組碼時,會在指令序列中插入記憶體屏障來禁止特定型別的處理器重排序。
5.什麼是記憶體屏障?volatile 和記憶體屏障有什麼關係?
記憶體屏障是一類同步屏障指令,是CPU或編譯器在對記憶體隨機訪問的操作中的一個同步點,使得此點之前的所有讀寫操作都執行後才可以開始執行此點之後的操作。
volatile是通過記憶體屏障實現可見性的。
6.什麼是 happens-before?volatile 和它有什麼關係?
因為jvm會對程式碼進行編譯優化,指令會出現重排序的情況,為了避免編譯優化對併發程式設計安全性的影響,需要happens-before規則定義一些禁止編譯優化的場景,保證併發程式設計的正確性。
訪問volatile變數在語句間建立了happens-before關係。當寫入一個volatile變數時,它與之後的該變數的讀操作建立了happens-before關係。
volatile變數規則:對一個volatile域的寫,happens-before於任意後續對這個volatile域的讀。
簡單來說,就是在volatile寫之後加入了一個StoreLoad屏障,防止後面的讀與前面的寫重排序了。這樣後面的執行緒讀到的,就是一個完整的物件。
7.如果單 CPU 的伺服器,是否使用 volatile 對程式有影響嗎?
即使是單核, 也有可能一個執行緒正在執行某個函式體的時候, CPU開小差跑去執行另一個執行緒了的函數了, 所以還是有影響的。
8.兩條語句,第一條是普通寫,第二條是 volatile 寫,其他執行緒對第一條普通寫可見嗎?
9.volatile int i;i++ 操作會有執行緒安全問題嗎?
會有的,volatitle保證了執行緒變數的可見性,但是不保證原子性。
10.volatile 能否替代 CAS?
不可能,cas保證了原子性。
11.為什麼 AQS 裡面的 state 使用了 CAS 還需要 volatile?
因為需要volatile的保證state的執行緒變數可見性。
12.Unsafe.putOrderedObject 是什麼?能否替代 volatile?
Oracle的JDK中提供了Unsafe. putOrderedObject,Unsafe. putOrderedInt,Unsafe. putOrderedLong這三個方法,JDK會在執行這三個方法時插入StoreStore記憶體屏障,避免發生寫操作重排序。而在Intel 64/IA-32架構下,StoreStore屏障並不需要,Java編譯器會將StoreStore屏障去除。比起寫入volatile變數之後執行StoreLoad屏障的巨大開銷,採用這種方法除了避免重排序而帶來的效能損失以外,不會帶來其它的效能開銷。
13.可以認為 CAS + volatile = synchronized 嗎?
不可以,因為cas操作存在一些問題。
ABA問題
1 因為CAS需要在操作值的時候,檢查值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。ABA問題的解決思路就是使用版本號。在變數前面追加上版本號,每次變數更新的時候把版本號加1,那麼A→B→A就會變成1A→2B→3A。
從Java 1.5開始,JDK的Atomic包裡提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法的作用是首先檢查當前引用是否等於預期引用,並且檢查當前標誌是否等於預期標誌,如果全部相等,則以原子方式將該引用和該標誌的值設定為給定的更新值。
2 迴圈時間長開銷大
一般CAS操作都是在不停的自旋,這個操作本身就有可能會失敗的,如果一直不停的失敗,則會給CPU帶來非常大的開銷。
3 只能保證一個共享變數的原子操作
看了CAS的實現就知道這個只能針對一個共享變數,如果是多個共享變數就只能使用synchronized。除此之外,可以考慮使用AtomicReference來包裝多個變數,通過這種方式來處理多個共享變數的情況。
14.解決可見性問題,使用了 synchronized 還需要 volatile 嗎?
需要。
一方面是因為synchronized是一種鎖機制,存在阻塞問題和效能問題,而volatile並不是鎖,所以不存在阻塞和效能問題。
另外一方面,因為volatile藉助了記憶體屏障來幫助其解決可見性和有序性問題,而記憶體屏障的使用還為其帶來了一個禁止指令重排的附件功能,所以在有些場景中是可以避免發生指令重排的問題的。
https://www.cnblogs.com/hollischuang/p/11386988.html 可見此篇博文。
15.利用 volatile 手寫一個懶漢式單例模式,並解釋為什麼這麼寫。
class SingletonClass{ private volatile static SingletonClass instance = null; private SingletonClass() {} public static SingletonClass getInstance() { if(instance==null) { synchronized ( SingletonClass.class) { if(instance==null) instance = new SingletonClass();//語句1 } } return instance; } }
語句1不是一個原子操作,在jvm中是三個操作。
1.給instance分配空間、
2.呼叫 Singleton 的建構函式來初始化、
3.將instance物件指向分配的記憶體空間(instance指向分配的記憶體空間後就不為null了);
在JVM中的及時編譯存在指令重排序的優化,也就是說不能保證1,2,3執行的順序,最終的執行順序可能是 1-2-3 也可能是 1-3-2。如果是1-3-2,則在 3 執行完畢、2 未執行之前,被執行緒二搶佔了,這時 instance 已經是非 null 了(但卻沒有初始化),所以執行緒二會直接返回 instance,然後使用,然後順理成章地報錯。
通過新增volatile就可以解決這種報錯,因為volatile可以保證1、2、3的執行順序,沒執行玩1、2就肯定不會執行3,也就是沒有執行完1、2instance一直為空,
16.使用 volatile 手寫一個生產者消費者程式。
public class Data { private String id; private String name; public Data(String id,String name){ this.id = id; this.name = name; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "Data [id=" + id + ", name=" + name + "]"; } }
public class Provider implements Runnable{ //共享緩衝區 private BlockingQueue<Data> queue; //多執行緒間是否啟動變數,有強制從主記憶體中重新整理的功能,及時返回執行緒狀態 private volatile boolean isRunning = true; //id生成器 private static AtomicInteger count = new AtomicInteger(); //隨機物件 private static Random r = new Random(); public Provider(BlockingQueue queue){ this.queue = queue; } @Override public void run() { while(isRunning){ //隨機休眠0-1000毫秒 表示獲取資料 try { Thread.sleep(r.nextInt(1000)); //獲取的資料進行累計 int id = count.incrementAndGet(); //比如通過一個getData()方法獲取了 Data data = new Data(Integer.toString(id),"資料"+id); System.out.println("當前執行緒:"+ Thread.currentThread().getName() + ",獲取了資料,id為:"+ id+ ",進行裝載到公共緩衝區中。。。"); if(!this.queue.offer(data,2,TimeUnit.SECONDS)){ System.out.print("提交緩衝區資料失敗"); } } catch (InterruptedException e) { e.printStackTrace(); } System.out.print("aaa"); } } public void stop(){ this.isRunning = false; } }
public class Consumer implements Runnable { private BlockingQueue<Data> queue; public Consumer(BlockingQueue queu){ this.queue = queu; } //隨機物件 private static Random r = new Random(); @Override public void run() { while(true){ try{ //獲取資料 Data data = this.queue.take(); //進行資料處理,休眠 0-1000毫秒模擬耗時 Thread.sleep(r.nextInt(1000)); System.out.print("當前消費執行緒"+Thread.currentThread().getName() +",消費成功,消費id為"+data.getId()); }catch(InterruptedException e){ e.printStackTrace(); } } } }
public class Main { public static void main(String[] args){ //記憶體緩衝區 BlockingQueue<Data> queue = new LinkedBlockingQueue<Data>(10); //生產者 Provider p1 = new Provider(queue); Provider p2 = new Provider(queue); Provider p3 = new Provider(queue); Consumer c1 = new Consumer(queue); Consumer c2 = new Consumer(queue); Consumer c3 = new Consumer(queue); //建立執行緒池,這是一個快取的執行緒池,可以建立無窮大的執行緒,沒有任務的時候不建立執行緒,空閒執行緒存活的時間為60s。 ExecutorService cachepool = Executors.newCachedThreadPool(); cachepool.execute(p1); cachepool.execute(p2); cachepool.execute(p3); cachepool.execute(c1); cachepool.execute(c2); cachepool.execute(c3); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } p1.stop(); p2.stop(); p3.stop(); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } }