volatile關鍵字三重功效

  1. 64位寫入原子性

舉一個簡單的例子,對於一個long型變量的賦值和取值操作而言,在多線程場景下,線程A調用set(100),線程B調用get(),在某些場景下,返回值可能不是100。

package com.lc.test02;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author liuchao
 * @date 2020/7/3
 */
public class Test01 {

    private long a = 0;

    public void set(long a) {//線程1設置值
        this.a = a;
    }

    public long get() {//線程2獲取值,返回值可能不是100
        return a;
    }

    public static void main(String[] args) {
        Test01 test = new Test01();
        ExecutorService executor = Executors.newFixedThreadPool(2);
        CountDownLatch latch = new CountDownLatch(1);
        executor.execute(() -> {
            try {
                latch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            long i = 100L;
            test.set(i);
            System.out.println("-----設置:" + i);
        });
        executor.execute(() -> {
            try {
                latch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("----獲取:" + test.get());
        });
        latch.countDown();
        executor.shutdown();
    }

}

 爲什麼獲取到的值有可能不是100呢,是因爲JVM規範並未要求64位的long和double寫入是原子性的,64位的數據有可能被拆分成兩個32位的數據寫入,這樣另一個線程拿到的數據有可能是32位的不完整的數據,如果在long屬性前面加上volatile關鍵字就可以解決此問題。

  1. 內存可見性

JVM將內存組織爲主內存和工作內存兩個部分。

針對主內存中的變量,線程A操作後線程B看到要經過的流程

線程A操作後數據存儲在線程A工作內存=》save到主內存=》線程B從主內存load到線程B工作內存

那在整個操作過程中是非原子性操作的,有可能線程A修改後是10,但是線程B讀取到的數據是非10,因爲線程A修改後的數據還未save到主內存,那要解決這個問題也比較簡單就是直接在屬性前面加上volatile關鍵字,也就是解決了內存可見性問題 

  1. 禁止重排序

在經典的單線程安全的寫法上DCL

//注意,此代碼有安全問題

package com.lc.test02;

/**
 * @author liuchao
 * @date 2020/7/3
 */
public class Test01 {

    private static Test01 instance;

    public static Test01 getInstance() {
        if (null == instance) {
            synchronized (instance) {//爲了性能驗證 使用lock
                if (null == instance) {
                    instance = new Test01();//有問題的代碼
                }
            }
        }
        return instance;
    }

}

上述的instance = new Test01();代碼有問題:其底層會分爲三個操作:

(1)分配一塊內存。

(2)在內存上初始化成員變量。

(3)把instance引用指向內存。 

在這三個操作中,操作(2)和操作(3)可能重排序,即先把instance指向內存,再初始化成員變量,因爲二者並沒有先後的依賴關係。此時,另外一個線程可能拿到一個未完全初始化的對象。這時,直接訪問裏面的成員變量,就可能出錯。這就是典型的“構造函數溢出”問題。解決辦法也很簡單,就是爲instance變量加上volatile修飾

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章