Volatile理解

  • JUC(java.util.concurrent)
    • 進(jìn)程和線程
      • 進(jìn)程:后臺(tái)運(yùn)行的程序(我們打開的一個(gè)軟件璧针,就是進(jìn)程)
      • 線程:程序執(zhí)行的最小單元,它依托進(jìn)程存在(同在一個(gè)軟件內(nèi)誉己,同時(shí)運(yùn)行窗口徽鼎,就是線程)
    • 并發(fā)和并行
      • 并發(fā):同時(shí)訪問某個(gè)東西,就是并發(fā)
      • 并行:一起做某些事情,就是并行
  • JUC下的三個(gè)包
    • java.util.concurrent
      • java.util.concurrent.atomic
      • java.util.concurrent.locks

1. volatile 理解

Volatile在日常的單線程環(huán)境是應(yīng)用不到的

  • Volatile是Java虛擬機(jī)提供的輕量級的同步機(jī)制(三大特性)
    • 保證可見性
    • 不保證原子性
    • 禁止指令重排

JMM是什么

JMM是Java內(nèi)存模型故硅,也就是Java Memory Model,簡稱JMM,本身是一種抽象的概念桐经,實(shí)際上并不存在,它描述的是一組規(guī)則或規(guī)范浙滤,通過這組規(guī)范定義了程序中各個(gè)變量(包括實(shí)例字段阴挣,靜態(tài)字段和構(gòu)成數(shù)組對象的元素)的訪問方式

JMM關(guān)于同步的規(guī)定:

  • 線程解鎖前,必須把共享變量的值刷新回主內(nèi)存
  • 線程解鎖前纺腊,必須讀取主內(nèi)存的最新值畔咧,到自己的工作內(nèi)存
  • 加鎖和解鎖是同一把鎖

由于JVM運(yùn)行程序的實(shí)體是線程,而每個(gè)線程創(chuàng)建時(shí)JVM都會(huì)為其創(chuàng)建一個(gè)工作內(nèi)存(有些地方稱為椧灸ぃ空間)誓沸,工作內(nèi)存是每個(gè)線程的私有數(shù)據(jù)區(qū)域,而Java內(nèi)存模型中規(guī)定所有變量都存儲(chǔ)在主內(nèi)存壹粟,主內(nèi)存是共享內(nèi)存區(qū)域拜隧,所有線程都可以訪問,但線程對變量的操作(讀取賦值等)必須在工作內(nèi)存中進(jìn)行,首先要將變量從主內(nèi)存拷貝到自己的工作內(nèi)存空間虹蓄,然后對變量進(jìn)行操作犀呼,操作完成后再將變量寫會(huì)主內(nèi)存,不能直接操作主內(nèi)存中的變量薇组,各個(gè)線程中的工作內(nèi)存中存儲(chǔ)著主內(nèi)存中的變量副本拷貝外臂,因此不同的線程間無法訪問對方的工作內(nèi)存,線程間的通信(傳值)必須通過主內(nèi)存來完成律胀,其簡要訪問過程:

[圖片上傳失敗...(image-d706ea-1589011343184)]

數(shù)據(jù)傳輸速率:硬盤 < 內(nèi)存 < < cache < CPU

上面提到了兩個(gè)概念:主內(nèi)存 和 工作內(nèi)存

  • 主內(nèi)存:就是計(jì)算機(jī)的內(nèi)存宋光,也就是經(jīng)常提到的8G內(nèi)存,16G內(nèi)存

  • 工作內(nèi)存:但我們實(shí)例化 new student炭菌,那么 age = 25 也是存儲(chǔ)在主內(nèi)存中

    • 當(dāng)同時(shí)有三個(gè)線程同時(shí)訪問 student中的age變量時(shí)罪佳,那么每個(gè)線程都會(huì)拷貝一份,到各自的工作內(nèi)存黑低,從而實(shí)現(xiàn)了變量的拷貝

[圖片上傳失敗...(image-ed4316-1589011343184)]

即:JMM內(nèi)存模型的可見性赘艳,指的是當(dāng)主內(nèi)存區(qū)域中的值被某個(gè)線程寫入更改后,其它線程會(huì)馬上知曉更改后的值克握,并重新得到更改后的值蕾管。

JMM的特性

JMM的三大特性,volatile只保證了兩個(gè)菩暗,即可見性和有序性掰曾,不滿足原子性

  • 可見性
  • 原子性
  • 有序性

可見性代碼驗(yàn)證

但我們對于成員變量沒有添加任何修飾時(shí),是無法感知其它線程修改后的值

package com.moxi.interview.study.thread;

/**
 * Volatile Java虛擬機(jī)提供的輕量級同步機(jī)制
 *
 * 可見性(及時(shí)通知)
 * 不保證原子性
 * 禁止指令重排
 *
 * @author: 陌溪
 * @create: 2020-03-09-15:58
 */

import java.util.concurrent.TimeUnit;

/**
 * 假設(shè)是主物理內(nèi)存
 */
class MyData {

    int number = 0;

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

/**
 * 驗(yàn)證volatile的可見性
 * 1. 假設(shè)int number = 0停团, number變量之前沒有添加volatile關(guān)鍵字修飾
 */
public class VolatileDemo {

    public static void main(String args []) {

        // 資源類
        MyData myData = new MyData();

        // AAA線程 實(shí)現(xiàn)了Runnable接口的旷坦,lambda表達(dá)式
        new Thread(() -> {

            System.out.println(Thread.currentThread().getName() + "\t come in");

            // 線程睡眠3秒,假設(shè)在進(jìn)行運(yùn)算
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 修改number的值
            myData.addTo60();

            // 輸出修改后的值
            System.out.println(Thread.currentThread().getName() + "\t update number value:" + myData.number);

        }, "AAA").start();

        while(myData.number == 0) {
            // main線程就一直在這里等待循環(huán)佑稠,直到number的值不等于零
        }

        // 按道理這個(gè)值是不可能打印出來的秒梅,因?yàn)橹骶€程運(yùn)行的時(shí)候,number的值為0讶坯,所以一直在循環(huán)
        // 如果能輸出這句話番电,說明AAA線程在睡眠3秒后,更新的number的值辆琅,重新寫入到主內(nèi)存漱办,并被main線程感知到了
        System.out.println(Thread.currentThread().getName() + "\t mission is over");

        /**
         * 最后輸出結(jié)果:
         * AAA   come in
         * AAA   update number value:60
         * 最后線程沒有停止,并行沒有輸出  mission is over 這句話婉烟,說明沒有用volatile修飾的變量娩井,是沒有可見性
         */

    }
}

輸出結(jié)果為
[圖片上傳失敗...(image-413bfa-1589011343184)]

最后線程沒有停止,并行沒有輸出 mission is over 這句話似袁,說明沒有用volatile修飾的變量洞辣,是沒有可見性

當(dāng)我們修改MyData類中的成員變量時(shí)咐刨,并且添加volatile關(guān)鍵字修飾

/**
 * 假設(shè)是主物理內(nèi)存
 */
class MyData {
    /**
     * volatile 修飾的關(guān)鍵字,是為了增加 主線程和線程之間的可見性扬霜,只要有一個(gè)線程修改了內(nèi)存中的值定鸟,其它線程也能馬上感知
     */
    volatile int number = 0;

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

最后輸出的結(jié)果為:

[圖片上傳失敗...(image-a3c200-1589011343184)]

主線程也執(zhí)行完畢了损俭,說明volatile修飾的變量怀喉,是具備JVM輕量級同步機(jī)制的,能夠感知其它線程的修改后的值率翅。


2. volatile 不保證原子性

前言

通過前面對JMM的介紹材原,我們知道沸久,各個(gè)線程對主內(nèi)存中共享變量的操作都是各個(gè)線程各自拷貝到自己的工作內(nèi)存進(jìn)行操作后在寫回到主內(nèi)存中的。

這就可能存在一個(gè)線程AAA修改了共享變量X的值余蟹,但是還未寫入主內(nèi)存時(shí)卷胯,另外一個(gè)線程BBB又對主內(nèi)存中同一共享變量X進(jìn)行操作,但此時(shí)A線程工作內(nèi)存中共享變量X對線程B來說是不可見威酒,這種工作內(nèi)存與主內(nèi)存同步延遲現(xiàn)象就造成了可見性問題窑睁。

原子性

不可分割,完整性兼搏,也就是說某個(gè)線程正在做某個(gè)具體業(yè)務(wù)時(shí)卵慰,中間不可以被加塞或者被分割,需要具體完成佛呻,要么同時(shí)成功,要么同時(shí)失敗病线。

數(shù)據(jù)庫也經(jīng)常提到事務(wù)具備原子性

代碼測試

為了測試volatile是否保證原子性吓著,我們創(chuàng)建了20個(gè)線程,然后每個(gè)線程分別循環(huán)1000次送挑,來調(diào)用number++的方法

        MyData myData = new MyData();

        // 創(chuàng)建10個(gè)線程绑莺,線程里面進(jìn)行1000次循環(huán)
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                // 里面
                for (int j = 0; j < 1000; j++) {
                    myData.addPlusPlus();
                }
            }, String.valueOf(i)).start();
        }

最后通過 Thread.activeCount(),來感知20個(gè)線程是否執(zhí)行完畢惕耕,這里判斷線程數(shù)是否大于2纺裁,為什么是2?因?yàn)槟J(rèn)是有兩個(gè)線程的司澎,一個(gè)main線程欺缘,一個(gè)gc線程

// 需要等待上面20個(gè)線程都計(jì)算完成后,在用main線程取得最終的結(jié)果值
while(Thread.activeCount() > 2) {
    // yield表示不執(zhí)行
    Thread.yield();
}

然后在線程執(zhí)行完畢后挤安,我們在查看number的值谚殊,假設(shè)volatile保證原子性的話,那么最后輸出的值應(yīng)該是

20 * 1000 = 20000,

完整代碼如下所示:


/**
 * Volatile Java虛擬機(jī)提供的輕量級同步機(jī)制
 *
 * 可見性(及時(shí)通知)
 * 不保證原子性
 * 禁止指令重排
 *
 * @author: 陌溪
 * @create: 2020-03-09-15:58
 */

import java.util.concurrent.TimeUnit;

/**
 * 假設(shè)是主物理內(nèi)存
 */
class MyData {
    /**
     * volatile 修飾的關(guān)鍵字蛤铜,是為了增加 主線程和線程之間的可見性嫩絮,只要有一個(gè)線程修改了內(nèi)存中的值丛肢,其它線程也能馬上感知
     */
    volatile int number = 0;

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

    /**
     * 注意,此時(shí)number 前面是加了volatile修飾
     */
    public void addPlusPlus() {
        number ++;
    }
}

/**
 * 驗(yàn)證volatile的可見性
 * 1剿干、 假設(shè)int number = 0蜂怎, number變量之前沒有添加volatile關(guān)鍵字修飾
 * 2、添加了volatile置尔,可以解決可見性問題
 *
 * 驗(yàn)證volatile不保證原子性
 * 1杠步、原子性指的是什么意思?
 */
public class VolatileDemo {

    public static void main(String args []) {

        MyData myData = new MyData();

        // 創(chuàng)建10個(gè)線程撰洗,線程里面進(jìn)行1000次循環(huán)
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                // 里面
                for (int j = 0; j < 1000; j++) {
                    myData.addPlusPlus();
                }
            }, String.valueOf(i)).start();
        }

        // 需要等待上面20個(gè)線程都計(jì)算完成后篮愉,在用main線程取得最終的結(jié)果值
        // 這里判斷線程數(shù)是否大于2,為什么是2差导?因?yàn)槟J(rèn)是有兩個(gè)線程的试躏,一個(gè)main線程,一個(gè)gc線程
        while(Thread.activeCount() > 2) {
            // yield表示不執(zhí)行
            Thread.yield();
        }

        // 查看最終的值
        // 假設(shè)volatile保證原子性设褐,那么輸出的值應(yīng)該為:  20 * 1000 = 20000
        System.out.println(Thread.currentThread().getName() + "\t finally number value: " + myData.number);

    }
}

最終結(jié)果我們會(huì)發(fā)現(xiàn)颠蕴,number輸出的值并沒有20000,而且是每次運(yùn)行的結(jié)果都不一致的助析,這說明了volatile修飾的變量不保證原子性

第一次:

[圖片上傳失敗...(image-6f9485-1589011343184)]

第二次:

[圖片上傳失敗...(image-e2cd8a-1589011343184)]

第三次:

[圖片上傳失敗...(image-59aaab-1589011343184)]

為什么出現(xiàn)數(shù)值丟失

[圖片上傳失敗...(image-80c1b6-1589011343184)]

各自線程在寫入主內(nèi)存的時(shí)候犀被,出現(xiàn)了數(shù)據(jù)的丟失,而引起的數(shù)值缺失的問題

下面我們將一個(gè)簡單的number++操作外冀,轉(zhuǎn)換為字節(jié)碼文件一探究竟

public class T1 {
    volatile int n = 0;
    public void add() {
        n++;
    }
}

轉(zhuǎn)換后的字節(jié)碼文件

public class com.moxi.interview.study.thread.T1 {
  volatile int n;

  public com.moxi.interview.study.thread.T1();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: iconst_0
       6: putfield      #2                  // Field n:I
       9: return

  public void add();
    Code:
       0: aload_0
       1: dup
       2: getfield      #2                  // Field n:I
       5: iconst_1
       6: iadd
       7: putfield      #2                  // Field n:I
      10: return
}

這里查看字節(jié)碼的操作寡键,是用到了IDEA的javap命令

我們首先,使用IDEA提供的External Tools雪隧,來擴(kuò)展javap命令

[圖片上傳失敗...(image-51f7bd-1589011343184)]

完成上述操作后西轩,我們在需要查看字節(jié)碼的文件下,右鍵選擇 External Tools即可

[圖片上傳失敗...(image-ed92a8-1589011343184)]

如果出現(xiàn)了找不到指定類脑沿,那是因?yàn)槲覀儎?chuàng)建的是spring boot的maven項(xiàng)目藕畔,我們之前需要執(zhí)行mvn package命令,進(jìn)行打包操作庄拇,將其編譯成class文件

移動(dòng)到底部注服,有一份字節(jié)碼指令對照表,方便我們進(jìn)行閱讀

下面我們就針對 add() 這個(gè)方法的字節(jié)碼文件進(jìn)行分析

  public void add();
    Code:
       0: aload_0
       1: dup
       2: getfield      #2    // Field n:I
       5: iconst_1
       6: iadd
       7: putfield      #2    // Field n:I
      10: return

我們能夠發(fā)現(xiàn) n++這條命令措近,被拆分成了3個(gè)指令

  • 執(zhí)行getfield 從主內(nèi)存拿到原始n
  • 執(zhí)行iadd 進(jìn)行加1操作
  • 執(zhí)行putfileld 把累加后的值寫回主內(nèi)存

假設(shè)我們沒有加 synchronized那么第一步就可能存在著溶弟,三個(gè)線程同時(shí)通過getfield命令,拿到主存中的 n值熄诡,然后三個(gè)線程可很,各自在自己的工作內(nèi)存中進(jìn)行加1操作,但他們并發(fā)進(jìn)行 iadd 命令的時(shí)候凰浮,因?yàn)橹荒芤粋€(gè)進(jìn)行寫我抠,所以其它操作會(huì)被掛起苇本,假設(shè)1線程,先進(jìn)行了寫操作菜拓,在寫完后瓣窄,volatile的可見性,應(yīng)該需要告訴其它兩個(gè)線程纳鼎,主內(nèi)存的值已經(jīng)被修改了俺夕,但是因?yàn)樘炝耍渌鼉蓚€(gè)線程贱鄙,陸續(xù)執(zhí)行 iadd命令劝贸,進(jìn)行寫入操作,這就造成了其他線程沒有接受到主內(nèi)存n的改變逗宁,從而覆蓋了原來的值映九,出現(xiàn)寫丟失,這樣也就讓最終的結(jié)果少于20000

如何解決

因此這也說明瞎颗,在多線程環(huán)境下 number ++ 在多線程環(huán)境下是非線程安全的件甥,解決的方法有哪些呢?

  • 在方法上加入 synchronized
    public synchronized void addPlusPlus() {
        number ++;
    }

運(yùn)行結(jié)果:

[圖片上傳失敗...(image-583e81-1589011343184)]

我們能夠發(fā)現(xiàn)引入synchronized關(guān)鍵字后哼拔,保證了該方法每次只能夠一個(gè)線程進(jìn)行訪問和操作引有,最終輸出的結(jié)果也就為20000

其它解決方法

上面的方法引入synchronized,雖然能夠保證原子性倦逐,但是為了解決number++譬正,而引入重量級的同步機(jī)制,有種 殺雞焉用牛刀

除了引用synchronized關(guān)鍵字外檬姥,還可以使用JUC下面的原子包裝類导帝,即剛剛的int類型的number,可以使用AtomicInteger來代替

    /**
     *  創(chuàng)建一個(gè)原子Integer包裝類穿铆,默認(rèn)為0
      */
    AtomicInteger atomicInteger = new AtomicInteger();

    public void addAtomic() {
        // 相當(dāng)于 atomicInter ++
        atomicInteger.getAndIncrement();
    }

然后同理,繼續(xù)剛剛的操作

        // 創(chuàng)建10個(gè)線程斋荞,線程里面進(jìn)行1000次循環(huán)
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                // 里面
                for (int j = 0; j < 1000; j++) {
                    myData.addPlusPlus();
                    myData.addAtomic();
                }
            }, String.valueOf(i)).start();
        }

最后輸出

        // 假設(shè)volatile保證原子性荞雏,那么輸出的值應(yīng)該為:  20 * 1000 = 20000
        System.out.println(Thread.currentThread().getName() + "\t finally number value: " + myData.number);
        System.out.println(Thread.currentThread().getName() + "\t finally atomicNumber value: " + myData.atomicInteger);

下面的結(jié)果,一個(gè)是引入synchronized平酿,一個(gè)是使用了原子包裝類AtomicInteger

[圖片上傳失敗...(image-b47bfe-1589011343184)]

字節(jié)碼指令表

為了方便閱讀JVM字節(jié)碼文件凤优,我從網(wǎng)上找了一份字節(jié)碼指令表

引用:https://segmentfault.com/a/1190000008722128


3. volatile 禁止指令重排

計(jì)算機(jī)在執(zhí)行程序時(shí),為了提高性能蜈彼,編譯器和處理器常常會(huì)對指令重排筑辨,一般分為以下三種:

源代碼 -> 編譯器優(yōu)化的重排 -> 指令并行的重排 -> 內(nèi)存系統(tǒng)的重排 -> 最終執(zhí)行指令

單線程環(huán)境里面確保最終執(zhí)行結(jié)果和代碼順序的結(jié)果一致

處理器在進(jìn)行重排序時(shí),必須要考慮指令之間的數(shù)據(jù)依賴性

多線程環(huán)境中線程交替執(zhí)行幸逆,由于編譯器優(yōu)化重排的存在棍辕,兩個(gè)線程中使用的變量能否保證一致性是無法確定的暮现,結(jié)果無法預(yù)測。

指令重排 - example 1

public void mySort() {
    int x = 11;
    int y = 12;
    x = x + 5;
    y = x * x;
}

按照正常單線程環(huán)境楚昭,執(zhí)行順序是 1 2 3 4

但是在多線程環(huán)境下栖袋,可能出現(xiàn)以下的順序:

  • 2 1 3 4
  • 1 3 2 4

上述的過程就可以當(dāng)做是指令的重排,即內(nèi)部執(zhí)行順序抚太,和我們的代碼順序不一樣

但是指令重排也是有限制的塘幅,即不會(huì)出現(xiàn)下面的順序

  • 4 3 2 1

因?yàn)樘幚砥髟谶M(jìn)行重排時(shí)候,必須考慮到指令之間的數(shù)據(jù)依賴性

因?yàn)椴襟E 4:需要依賴于 y的申明尿贫,以及x的申明电媳,故因?yàn)榇嬖跀?shù)據(jù)依賴,無法首先執(zhí)行

例子

int a,b,x,y = 0

線程1 線程2
x = a; y = b;
b = 1; a = 2;
x = 0; y = 0

因?yàn)樯厦娴拇a庆亡,不存在數(shù)據(jù)的依賴性匾乓,因此編譯器可能對數(shù)據(jù)進(jìn)行重排

線程1 線程2
b = 1; a = 2;
x = a; y = b;
x = 2; y = 1

這樣造成的結(jié)果,和最開始的就不一致了身冀,這就是導(dǎo)致重排后钝尸,結(jié)果和最開始的不一樣,因此為了防止這種結(jié)果出現(xiàn)搂根,volatile就規(guī)定禁止指令重排珍促,為了保證數(shù)據(jù)的一致性

指令重排 - example 2

比如下面這段代碼

/**
 * ResortSeqDemo
 *
 * @author: 陌溪
 * @create: 2020-03-10-16:08
 */
public class ResortSeqDemo {
    int a= 0;
    boolean flag = false;

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

    public void method02() {
        if(flag) {
            a = a + 5;
            System.out.println("reValue:" + a);
        }
    }
}

我們按照正常的順序,分別調(diào)用method01() 和 method02() 那么剩愧,最終輸出就是 a = 6

但是如果在多線程環(huán)境下猪叙,因?yàn)榉椒? 和 方法2,他們之間不能存在數(shù)據(jù)依賴的問題仁卷,因此原先的順序可能是

a = 1;
flag = true;

a = a + 5;
System.out.println("reValue:" + a);
        

但是在經(jīng)過編譯器穴翩,指令,或者內(nèi)存的重排后锦积,可能會(huì)出現(xiàn)這樣的情況

flag = true;

a = a + 5;
System.out.println("reValue:" + a);

a = 1;

也就是先執(zhí)行 flag = true后芒帕,另外一個(gè)線程馬上調(diào)用方法2,滿足 flag的判斷丰介,最終讓a + 5背蟆,結(jié)果為5,這樣同樣出現(xiàn)了數(shù)據(jù)不一致的問題

為什么會(huì)出現(xiàn)這個(gè)結(jié)果:多線程環(huán)境中線程交替執(zhí)行哮幢,由于編譯器優(yōu)化重排的存在带膀,兩個(gè)線程中使用的變量能否保證一致性是無法確定的,結(jié)果無法預(yù)測橙垢。

這樣就需要通過volatile來修飾垛叨,來保證線程安全性

Volatile針對指令重排做了啥

Volatile實(shí)現(xiàn)禁止指令重排優(yōu)化,從而避免了多線程環(huán)境下程序出現(xiàn)亂序執(zhí)行的現(xiàn)象

首先了解一個(gè)概念柜某,內(nèi)存屏障(Memory Barrier)又稱內(nèi)存柵欄嗽元,是一個(gè)CPU指令敛纲,它的作用有兩個(gè):

  • 保證特定操作的順序
  • 保證某些變量的內(nèi)存可見性(利用該特性實(shí)現(xiàn)volatile的內(nèi)存可見性)

由于編譯器和處理器都能執(zhí)行指令重排的優(yōu)化,如果在指令間插入一條Memory Barrier則會(huì)告訴編譯器和CPU还棱,不管什么指令都不能和這條Memory Barrier指令重排序载慈,也就是說 通過插入內(nèi)存屏障禁止在內(nèi)存屏障前后的指令執(zhí)行重排序優(yōu)化。 內(nèi)存屏障另外一個(gè)作用是刷新出各種CPU的緩存數(shù)珍手,因此任何CPU上的線程都能讀取到這些數(shù)據(jù)的最新版本办铡。

[圖片上傳失敗...(image-93dc00-1589011343184)]

也就是過在Volatile的寫 和 讀的時(shí)候,加入屏障琳要,防止出現(xiàn)指令重排的

線程安全獲得保證

工作內(nèi)存與主內(nèi)存同步延遲現(xiàn)象導(dǎo)致的可見性問題

  • 可通過synchronized或volatile關(guān)鍵字解決寡具,他們都可以使一個(gè)線程修改后的變量立即對其它線程可見

對于指令重排導(dǎo)致的可見性問題和有序性問題

  • 可以使用volatile關(guān)鍵字解決,因?yàn)関olatile關(guān)鍵字的另一個(gè)作用就是禁止重排序優(yōu)化

4. volatile 的應(yīng)用

單例模式DCL代碼

首先回顧一下稚补,單線程下的單例模式代碼

/**
 * SingletonDemo(單例模式)
 *
 * @author: 陌溪
 * @create: 2020-03-10-16:40
 */
public class SingletonDemo {

    private static SingletonDemo instance = null;

    private SingletonDemo () {
        System.out.println(Thread.currentThread().getName() + "\t 我是構(gòu)造方法SingletonDemo");
    }

    public static SingletonDemo getInstance() {
        if(instance == null) {
            instance = new SingletonDemo();
        }
        return instance;
    }

    public static void main(String[] args) {
        // 這里的 == 是比較內(nèi)存地址
        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
    }
}

最后輸出的結(jié)果
[圖片上傳失敗...(image-fae0eb-1589011343184)]

但是在多線程的環(huán)境下童叠,我們的單例模式是否還是同一個(gè)對象了

/**
 * SingletonDemo(單例模式)
 *
 * @author: 陌溪
 * @create: 2020-03-10-16:40
 */
public class SingletonDemo {

    private static SingletonDemo instance = null;

    private SingletonDemo () {
        System.out.println(Thread.currentThread().getName() + "\t 我是構(gòu)造方法SingletonDemo");
    }

    public static SingletonDemo getInstance() {
        if(instance == null) {
            instance = new SingletonDemo();
        }
        return instance;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                SingletonDemo.getInstance();
            }, String.valueOf(i)).start();
        }
    }
}

從下面的結(jié)果我們可以看出,我們通過SingletonDemo.getInstance() 獲取到的對象课幕,并不是同一個(gè)厦坛,而是被下面幾個(gè)線程都進(jìn)行了創(chuàng)建,那么在多線程環(huán)境下乍惊,單例模式如何保證呢杜秸?

[圖片上傳失敗...(image-480a34-1589011343184)]

解決方法1

引入synchronized關(guān)鍵字

    public synchronized static SingletonDemo getInstance() {
        if(instance == null) {
            instance = new SingletonDemo();
        }
        return instance;
    }

輸出結(jié)果

[圖片上傳失敗...(image-c6540a-1589011343184)]

我們能夠發(fā)現(xiàn),通過引入Synchronized關(guān)鍵字润绎,能夠解決高并發(fā)環(huán)境下的單例模式問題

但是synchronized屬于重量級的同步機(jī)制撬碟,它只允許一個(gè)線程同時(shí)訪問獲取實(shí)例的方法,但是為了保證數(shù)據(jù)一致性莉撇,而減低了并發(fā)性呢蛤,因此采用的比較少

解決方法2

通過引入DCL Double Check Lock 雙端檢鎖機(jī)制

就是在進(jìn)來和出去的時(shí)候,進(jìn)行檢測

    public static SingletonDemo getInstance() {
        if(instance == null) {
            // 同步代碼段的時(shí)候棍郎,進(jìn)行檢測
            synchronized (SingletonDemo.class) {
                if(instance == null) {
                    instance = new SingletonDemo();
                }
            }
        }
        return instance;
    }

最后輸出的結(jié)果為:

[圖片上傳失敗...(image-a5db72-1589011343184)]

從輸出結(jié)果來看其障,確實(shí)能夠保證單例模式的正確性,但是上面的方法還是存在問題的

DCL(雙端檢鎖)機(jī)制不一定是線程安全的涂佃,原因是有指令重排的存在静秆,加入volatile可以禁止指令重排

原因是在某一個(gè)線程執(zhí)行到第一次檢測的時(shí)候,讀取到 instance 不為null巡李,instance的引用對象可能沒有完成實(shí)例化。因?yàn)?instance = new SingletonDemo()扶认;可以分為以下三步進(jìn)行完成:

  • memory = allocate(); // 1侨拦、分配對象內(nèi)存空間
  • instance(memory); // 2、初始化對象
  • instance = memory; // 3辐宾、設(shè)置instance指向剛剛分配的內(nèi)存地址狱从,此時(shí)instance != null

但是我們通過上面的三個(gè)步驟膨蛮,能夠發(fā)現(xiàn),步驟2 和 步驟3之間不存在 數(shù)據(jù)依賴關(guān)系季研,而且無論重排前 還是重排后敞葛,程序的執(zhí)行結(jié)果在單線程中并沒有改變,因此這種重排優(yōu)化是允許的与涡。

  • memory = allocate(); // 1惹谐、分配對象內(nèi)存空間
  • instance = memory; // 3、設(shè)置instance指向剛剛分配的內(nèi)存地址驼卖,此時(shí)instance != null氨肌,但是對象還沒有初始化完成
  • instance(memory); // 2、初始化對象

這樣就會(huì)造成什么問題呢酌畜?

也就是當(dāng)我們執(zhí)行到重排后的步驟2怎囚,試圖獲取instance的時(shí)候,會(huì)得到null桥胞,因?yàn)閷ο蟮某跏蓟€沒有完成恳守,而是在重排后的步驟3才完成,因此執(zhí)行單例模式的代碼時(shí)候贩虾,就會(huì)重新在創(chuàng)建一個(gè)instance實(shí)例

指令重排只會(huì)保證串行語義的執(zhí)行一致性(單線程)催烘,但并不會(huì)關(guān)系多線程間的語義一致性

所以當(dāng)一條線程訪問instance不為null時(shí),由于instance實(shí)例未必已初始化完成整胃,這就造成了線程安全的問題

所以需要引入volatile颗圣,來保證出現(xiàn)指令重排的問題,從而保證單例模式的線程安全性

private static volatile SingletonDemo instance = null;

最終代碼

/**
 * SingletonDemo(單例模式)
 *
 * @author: 陌溪
 * @create: 2020-03-10-16:40
 */
public class SingletonDemo {

    private static volatile SingletonDemo instance = null;

    private SingletonDemo () {
        System.out.println(Thread.currentThread().getName() + "\t 我是構(gòu)造方法SingletonDemo");
    }

    public static SingletonDemo getInstance() {
        if(instance == null) {
            // 同步代碼段的時(shí)候屁使,進(jìn)行檢測
            synchronized (SingletonDemo.class) {
                if(instance == null) {
                    instance = new SingletonDemo();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
//        // 這里的 == 是比較內(nèi)存地址
//        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
//        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
//        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
//        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());

        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                SingletonDemo.getInstance();
            }, String.valueOf(i)).start();
        }
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末在岂,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子蛮寂,更是在濱河造成了極大的恐慌蔽午,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件酬蹋,死亡現(xiàn)場離奇詭異及老,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)范抓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進(jìn)店門骄恶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人匕垫,你說我怎么就攤上這事僧鲁。” “怎么了?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵寞秃,是天一觀的道長斟叼。 經(jīng)常有香客問我,道長春寿,這世上最難降的妖魔是什么朗涩? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮绑改,結(jié)果婚禮上谢床,老公的妹妹穿的比我還像新娘。我一直安慰自己绢淀,他們只是感情好萤悴,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著皆的,像睡著了一般覆履。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上费薄,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天硝全,我揣著相機(jī)與錄音,去河邊找鬼楞抡。 笑死伟众,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的召廷。 我是一名探鬼主播凳厢,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼竞慢!你這毒婦竟也來了先紫?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤筹煮,失蹤者是張志新(化名)和其女友劉穎遮精,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體败潦,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡本冲,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了劫扒。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片檬洞。...
    茶點(diǎn)故事閱讀 38,617評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖沟饥,靈堂內(nèi)的尸體忽然破棺而出疮胖,到底是詐尸還是另有隱情环戈,我是刑警寧澤,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布澎灸,位于F島的核電站,受9級特大地震影響遮晚,放射性物質(zhì)發(fā)生泄漏性昭。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一县遣、第九天 我趴在偏房一處隱蔽的房頂上張望糜颠。 院中可真熱鬧,春花似錦萧求、人聲如沸其兴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽元旬。三九已至,卻和暖如春守问,著一層夾襖步出監(jiān)牢的瞬間匀归,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工耗帕, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留穆端,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓仿便,卻偏偏與公主長得像体啰,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子嗽仪,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評論 2 348