java面試-談談你對volatile的理解


一、volatile特性:

volatile是Java虛擬機提供的輕量級的同步機制。主要有三大特性:

  • 保證可見性
  • 不保證原子性
  • 禁止指令重排序

1、保證可見性

1)代碼演示

AAA線程修改變量number的值為60,main線程獲取到的number值是0,就一直循環等待。

原因:int number = 0;number變量之前沒有添加volatile關鍵字,沒有可見性。添加volatile關鍵字,可以解決可見性問題。

public class VolatileDemo {
    int number = 0;

    public void addTo60() {
        this.number = 60;
    }

    //volatile可以保證可見性,及時通知其他線程,主物理內存的值已經被修改
    public static void main(String[] args) {
        VolatileDemo volatileDemo = new VolatileDemo();
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " come in");
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            volatileDemo.addTo60();
            System.out.println(Thread.currentThread().getName() + " update number value:" + volatileDemo.number);

        }, "AAA").start();

        //第2個線程是main線程
        while (volatileDemo.number == 0) {
            //main線程就一直等待循環,直到number的值不等於0

        }
        System.out.println(Thread.currentThread().getName() + " mission is over, main thread number value:" + volatileDemo.number);
    }
}  

2)volatile是如何來保證可見性的呢?

如果對聲明了volatile的變量進行寫操作,JVM就會向處理器發送一條Lock前綴的指令。

  • 將這個變量所在緩存行的數據寫回到系統內存。
  • 這個寫回內存的操作會使在其他CPU里緩存了該內存地址的數據無效。

2、不保證原子性

1)代碼演示

 volatile修飾number,進行number++操作,每次執行number的返回結果都不一樣

public class VolatileDemo {
    volatile int number = 0;

    public void increase() {
        number++;
    }

    public static void main(String[] args) {
        VolatileDemo volatileDemo = new VolatileDemo();
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    volatileDemo.increase();
                }
            }).start();
        }

        // 默認有 main 線程和 gc 線程
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName() + " finally number value:" + volatileDemo.number);
    }
}

2)volatile為什么不保證原子性?

 number++被拆分成3個指令:

getfield  從主內存中拿到原始值
iadd 在線程工作內存中進行加1操作
putfield 把累加后的值寫回主內存
如果第二個線程在第一個線程讀取舊值和寫回新值期間讀取number的值,
那么第二個線程就會與第一個線程看到同一個值,並執行相同值的加1操作,這也就造成了線程安全失敗。

3)如何解決原子性問題

  • CAS機制:AtomicInteger number = AtomicInteger(0)
  • 鎖機制:synchronized、Lock

3、禁止指令重排序

volatile的寫-讀與鎖的釋放-獲取有相同的內存效果。

volatile寫-讀的內存語義:

當寫一個volatile變量時,JMM會把線程A對應的本地內存中的共享變量值刷新到主內存。
當讀一個volatile變量時,JMM會把線程B對應的本地內存置為無效。線程接下來將從主內存中讀取共享變量。

線程A寫一個volatile變量,隨后線程B讀這個volatile變量,實質上是線程A通過主內存向線程B發送消息
public class VolatileExample {
    int a = 0;

    volatile boolean flag = false;

    public void writer() {
        a = 1;
        flag = true;
    }


    public void reader() {
        if (flag) {
            System.out.println("resultValue:" + a);
        }

    }
}

 

為了實現volatile的內存語義,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序

volatile寫插入內存屏障:

volatile讀插入內存屏障:

 二、你在哪些地方用到過volatile

1、單例模式(雙重檢查鎖DCL)

以下代碼不一定線程安全,原因是有指令重排序的存在,某個線程執行到第一次檢測,讀取到的instance不為null時,instance的引用對象可能沒有完成初始化
因為instance = new SingletonDemo();可以分為以下3步完成(偽代碼)
memory = allocate(); //1.分配對象內存空間
instance(memory); //2.初始化對象
instance = memory; //3.設置instance指向剛分配的內存地址,此時instance!=null
步驟2和步驟3間可能會重排序
使用volatile禁止指令重排序,對volatile變量的寫操作都先行發生於后面對這個變量的讀操作
public class SignletonDemo {

    private static SignletonDemo instance;

    private SignletonDemo() {
        System.out.println(Thread.currentThread().getName() + " 構造方法SingletonDemo");
    }

    public static SignletonDemo getInstance() {
        //第一次檢測
        if (instance == null) {
            //同步
            synchronized (SignletonDemo.class) {
                if (instance == null) {
                    //多線程環境下可能會出現問題的地方
                    instance = new SignletonDemo();
                }
            }
        }
        return instance;
    }
}

附:六種常見的單例模式

2、讀寫鎖手寫緩存

3、CAS JUC包中大量使用volatile

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM