volatile超详细讲解
目錄
寫在前面
一、什么是volatile
二、JVM(java虛擬機(jī))、JMM(java內(nèi)存模型)
三、volatile內(nèi)存可見性驗證
四、可見性說明
五、volitale不保證原子性驗證
六、volatile不保證原子性理論解釋
七:volatile不保證原子性問題解決
八、volatile指令重排
九、單例模式在多線程環(huán)境下可能存在安全問題
寫在前面
? ? 很多小伙伴相信都會聽說過volatile關(guān)鍵字,但是這個關(guān)鍵字有什么作用呢?也許大體也能明白,但是如果碰上較真的面試官,可能會直接蒙了。
? ? 今天就給大家充分分析一下volatile關(guān)鍵字到底有什么作用~
一、什么是volatile
? ? volatile是java的一個關(guān)鍵字,volatile是Java虛擬機(jī)提供的輕量級的同步機(jī)制。
? ? volatile有三個特性:1.保證可見性。2.不保證原子性。3.禁止指令重排序。
二、JVM(java虛擬機(jī))、JMM(java內(nèi)存模型)
? ? JMM(Java內(nèi)存模型 Java Memory Model,簡稱JMM)本身是一種抽象的概念并不真實存在,它描述的是一組規(guī)則或規(guī)范,通過這組規(guī)范定義了程序中各個變量(包括實例字段,靜態(tài)字段和構(gòu)成數(shù)組對象的元素)的訪問方式。
? ? JMM關(guān)于同步的規(guī)定:
1線程解鎖前,必須把共享變量的值刷新回主內(nèi)存
2線程加鎖前,必須讀取主內(nèi)存的最新值到自己的工作內(nèi)存
3加鎖解鎖是同一把鎖
? ? 由于JMM運行程序的實體是線程,而每個線程創(chuàng)建時JM都會為其創(chuàng)建一個工作內(nèi)存(有些地方稱為棧空間),工作內(nèi)存是每個線程的私有數(shù)據(jù)區(qū)域,而Java內(nèi)存模型中規(guī)定所有變量都存儲在主內(nèi)存,主內(nèi)存是共享內(nèi)存區(qū)域,所有線程都可以訪問,但線程對變量的操作(讀取賦值等)必須在工作內(nèi)存中進(jìn)行,首先要將變量從主內(nèi)存拷貝的自己的工作內(nèi)存空間,然后對變量進(jìn)行操作,操作完成后再將變量寫回主內(nèi)存,不能直接操作主內(nèi)存中的變量,各個線程中的工作內(nèi)存中存儲著主內(nèi)存中的變量副本拷貝,因此不同的線程間無法訪問對方的工作內(nèi)存,線程間的通信(傳值)必須通過主內(nèi)存來完成,其簡要訪問過程如下圖:
三、volatile內(nèi)存可見性驗證
/** * volatile驗證內(nèi)存的可見性 */ public class VolatileTest {public static void main(String[] args) {Dt1 dt1 = new Dt1();//新線程,三秒中后將num值設(shè)置為100new Thread(() -> {System.out.println(Thread.currentThread().getName() + " start");try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}dt1.setDt();System.out.println(Thread.currentThread().getName() + " date 0 -> " + dt1.num);}, "Thread1").start();while(dt1.num == 0){//num不變的話會一直卡在這//num不用volatile修飾的話,會一直卡在這,main的最后一段不會打印}System.out.println("main 結(jié)束,num值改變了!");} }/** * 1.不加volatile,num沒有可見性。 * 2.添加volatile可以解決可見性問題 */ class Dt1{//volatile int num = 0;int num = 0;public void setDt(){this.num = 100;} }四、可見性說明
? ? 各個線程對主內(nèi)存中共享變量的操作都是各個線程各自拷貝到自己的工作內(nèi)存進(jìn)行操作后再寫回到主內(nèi)存中的。
? ? 這就可能存在一個線程AAA修改了共享變量X的值但還未寫回主內(nèi)存時,另一個線程BBB又對主內(nèi)存中同一個共享變量X進(jìn)行操作,但此時AAA線程工作內(nèi)存中共享變量X對線程BBB來說并不可見,這種工作內(nèi)存與主內(nèi)存同步延遲現(xiàn)象就造成了可見性問題。
五、volitale不保證原子性驗證
/** * 驗證volatile不保證數(shù)據(jù)的原子性 * 什么是原子性?在做某個業(yè)務(wù)時,中間的邏輯不可以被分割。 */ public class VolatileTest2 {public static void main(String[] args) {Dt2 dt2 = new Dt2();//20個線程,每個線程將num加1000次for (int i = 0;i < 20; i++){new Thread(() -> {for (int j = 0; j < 1000; j++) {dt2.addDt();}}).start();}while (Thread.activeCount() > 2){Thread.yield();}System.out.println(dt2.num);//發(fā)現(xiàn),最后的打印結(jié)果并不是20000!所以volatile并不會保證++操作的原子性} }/** * 用volatile修飾,++操作并不會保證原子性 */ class Dt2{volatile int num = 0;//int num = 0;public void addDt(){this.num ++;} }六、volatile不保證原子性理論解釋
num++會被分解成3個機(jī)器指令,三個指令并不是一個原子操作,所以volitale并不會保證操作的原子性:
七:volatile不保證原子性問題解決
1.使用synchronized(大材小用)
class Dt2{int num = 0;public synchronized void addDt(){this.num ++;} }2.atomic原子類
import java.util.concurrent.atomic.AtomicInteger;/** * 使用AtomicInteger原子類 */ public class VolatileTestAtomicInteger {public static void main(String[] args) {Dt3 dt3 = new Dt3();//20個線程,每個線程將num加1000次for (int i = 0;i < 20; i++){new Thread(() -> {for (int j = 0; j < 1000; j++) {dt3.addDt();}}).start();}while (Thread.activeCount() > 2){Thread.yield();}System.out.println(dt3.num);//發(fā)現(xiàn),最后的打印結(jié)果就是20000!} }class Dt3{AtomicInteger num = new AtomicInteger();//默認(rèn)是0public void addDt(){num.getAndIncrement();} }八、volatile指令重排
1.什么是有序性: 計算機(jī)在執(zhí)行程序時,為了提高性能,編譯器和處理器常常會對指令做重排,一般分為以下3種: 源代碼->編譯器優(yōu)化的重排->指令并行的重排->內(nèi)存系統(tǒng)的重排->最終執(zhí)行的指令。 單線程環(huán)境里面確保程序最終執(zhí)行結(jié)果和代碼順序執(zhí)行的結(jié)果一致。 處理器在進(jìn)行重排序時必須要考慮指令之間的數(shù)據(jù)依賴性(先有你爹才能有你……)。 多線程環(huán)境中線程交替執(zhí)行,由于編譯器優(yōu)化重排的存在,兩個線程中使用的變量能否保證一致性是無法確定的,結(jié)果無法預(yù)測。 (舉例:高考做卷子,優(yōu)先做會的,并不是根據(jù)題目來順序做……) 2.指令重排案例: int x = 11; // 語句1 int y = 12; // 語句2 x = x + 5; // 語句3 y = x * x; // 語句4// 指令重排之后,可能會出現(xiàn)以下幾種情況(對結(jié)果并不會有影響) 1234 2134 1324//不可能出現(xiàn)4123。因為數(shù)據(jù)依賴性,語句4依賴于x聲明的值、x計算后的值、y的聲明。 /** * method2和method2多線程情況下,有可能會出現(xiàn)a=6,也有可能出現(xiàn)a=5,因為語句1和語句2指令重排 */ public class ReSortSeqDemo{int a = 0;boolean flag = false;public void method1(){a = 1; // 語句1flag = true; // 語句2}// 多線程環(huán)境中線程交替執(zhí)行,由于編譯器優(yōu)化重排的存在,兩個線程中使用的變量能否保證一致性是無法確定的,結(jié)果無法預(yù)測。public void method2(){if(flag){a = a + 5; // 語句3System.out.println("value a = " + a);}} } 3.指令重排小總結(jié) volatile實現(xiàn)禁止指令重排優(yōu)化,從而避免多線程環(huán)境下程序出現(xiàn)亂序執(zhí)行的現(xiàn)象。 先了解一個概念,內(nèi)存屏障(Memory?Barrier)又稱內(nèi)存柵欄,是一個CPU指令,它的作用有兩個: 一是保證特定操作的執(zhí)行順序, 二是保證某些變量的內(nèi)存可見性(利用該特性實現(xiàn)volatile的內(nèi)存可見性)。 由于編譯器和處理器都能執(zhí)行指令重排優(yōu)化。如果在指令間插入一條Memory?Barrier則會告訴編譯器和CPU,不管什么指令都不能和這條Memory?Barrier指令重排序,也就是說通過插入內(nèi)存屏障禁止在內(nèi)存屏障前后的指令執(zhí)行重排序優(yōu)化。內(nèi)存屏障另外一個作用是強(qiáng)制刷出各種CPU的緩存數(shù)據(jù),因此任何CPU上的線程都能讀取到這些數(shù)據(jù)的最新版本。 4.線程安全性獲得保證 工作內(nèi)存與主內(nèi)存同步延遲現(xiàn)象導(dǎo)致的可見性問題: 可以使用synchronized或volatile關(guān)鍵字解決,它們都可以使一個線程修改后的變量立即對其他線程可見。 對于指令重排導(dǎo)致的可見性問題和有序性問題: 可以利用volatile關(guān)鍵字解決,因為volatile的另外一個作用就是禁止重排序優(yōu)化。九、單例模式在多線程環(huán)境下可能存在安全問題
1.代碼示例
public class SingletonDemo {//雙端檢鎖需要使用volatileprivate volatile static SingletonDemo instance = null;private SingletonDemo(){System.out.println("我是私有構(gòu)造方法");}//這里加synchronized也可以實現(xiàn)單例,但是太重public static SingletonDemo getInstance(){if(instance == null){instance = new SingletonDemo();}return instance;}//DCL(Double check Lock 雙端檢鎖機(jī)制)public static SingletonDemo getInstance2(){if(instance == null){synchronized (SingletonDemo.class){if(instance == null){instance = new SingletonDemo();}}}return instance;}public static void main(String[] args) {//單線程下的單例模式,私有構(gòu)造方法只執(zhí)行一次 // System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance()); // System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance()); // System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());//并發(fā)模式下 // for (int i = 0; i < 10; i++) { // new Thread(() -> { // //并發(fā)下,單例模式的構(gòu)造方法會被調(diào)用很多次,有線程不安全問題 // SingletonDemo.getInstance(); // }, String.valueOf(i)).start(); // }//雙端檢鎖for (int i = 0; i < 10; i++) {new Thread(() -> {//并發(fā)下,單例模式的構(gòu)造方法會被調(diào)用很多次,有線程不安全問題SingletonDemo.getInstance2();}, String.valueOf(i)).start();}} } 2.為什么DCL(雙端檢鎖機(jī)制)一定要用volatile? DCL(雙端檢鎖)機(jī)制不一定線程安全,原因是有指令重排序的存在,加入volatile可以禁止指令重排。 原因在于某一個線程執(zhí)行到第一次檢測,讀取到的instance不為null時,instance的引用對象可能沒有完成初始化。 instance = new SingletonDemo();// 可以分為以下3步完成(偽代碼) memory = allocate(); // 1.分配對象內(nèi)存空間 instance(memory); // 2.初始化對象 instance = memory; // 3.設(shè)置instance指向剛分配的地址,此時instance!=null。 步驟2和步驟3不存在數(shù)據(jù)依賴關(guān)系,而且無論重排前還是重排后程序的執(zhí)行結(jié)果在單線程中并沒有改變,因此這種重排優(yōu)化是允許的。 假如重排后: instance = new SingletonDemo();// 可以分為以下3步完成(偽代碼) memory = allocate(); // 1.分配對象內(nèi)存空間 instance = memory; // 3.設(shè)置instance指向剛分配的地址,此時還沒有完成初始化!此時如果有另一個線程也同時執(zhí)行到這,導(dǎo)致這倆線程初始化的對象并不是同一個對象。 instance(memory); // 2.初始化對象? ? 但是指令重排只會保證串行語義的執(zhí)行的一致性(單線程),但并不會關(guān)心多線程間的語義一致性。
? ? 所以當(dāng)一條線程訪問instance不為null時,由于指令重排序,instance實例未必已初始化完成,也就造成了線程安全問題,導(dǎo)致兩次取得的單例對象并不是同一個對象。
總結(jié)
以上是生活随笔為你收集整理的volatile超详细讲解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 让你了解什么是内存屏障
- 下一篇: 我知道乐观锁,但是我的确不知道CAS啊,