JUC併發編程(十七)-volatile 詳解

17、volatile

volatile是不錯的機制,但是也不能保證原子性。

17.1. volatile 可見性

代碼驗證可見性

在這裏插入圖片描述

package com.interview.concurrent.volatiles;

import java.util.concurrent.TimeUnit;

/**
 *  @description:測試volatile的可見性
 *  @author yangxj
 *  @date 2020/2/26 17:54
 */
public class VolatileVisibility {
    // volatile 讀取的時候去主內存中讀取在最新值!
    private volatile static int num = 0;

    public static void main(String[] args) throws InterruptedException { // Main線程

        new Thread(()->{
            /**
             *  @description:
             *  由於num添加了volatile,所以線程每次讀取都會去主內存中讀取
             *  @author yangxj
             *  @date 2020/2/26 17:54
             */
            while (num==0){

            }
        }).start();

        TimeUnit.SECONDS.sleep(1);

        num = 1;
        System.out.println(num);

    }
}

17.2. volatile 不保證原子性

驗證 volatile 不保證原子性

17.2.1. volatile 不保證原子性的原因分析

原子性:ACID 不可分割!完整,要麼同時失敗,要麼同時成功!

package com.interview.concurrent.volatiles;

/**
 * @author yangxj
 * @description 描述:驗證volatile的原子性
 * @date 2020/2/26 17:55
 */
public class VolatileAcid {
    private volatile static  int num = 0;

    public static void main(String[] args) {
        for (int i = 1; i <= 20; i++) {
            new Thread(()->{
                for (int j = 1; j <= 1000; j++) {
                    add();  // 20 * 1000 = 20000
                }
            },String.valueOf(i)).start();
        }
        // main線程等待上面執行完成,判斷線程存活數   2
        while (Thread.activeCount()>2){ // main  gc
            //線程放棄當前分得的 CPU 時間,但是不使線程阻塞
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName()+" "+num);
    }

    public static void  add(){
        num++;
    }
}

運行效果如下:

在這裏插入圖片描述

運行效果並不是我們預期中的20000,這是什麼原因呢?

因爲numm++ 不是原子操作,而volatile也不保證原子性操作。

分析VolatileAcid類在堆中運行的字節碼

使用命令查看類運行的字節碼

javap -c xxx.class

在這裏插入圖片描述

num++被拆分成三部分,在這三者之間又會有其他進程進來讀取原始值,做加1操作。怎麼解決呢?看下文分解

17.2.2. volatile 不保證原子性解決方案

解決方案:

1、使用synchronized:前面已經說過;

2、使用原子性類工具java.util.concurrent.atomic

在這裏插入圖片描述

使用atomic解決原子性問題,示例代碼如下:

package com.interview.concurrent.volatiles;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author yangxj
 * @description 描述:使用atomic解決原子性
 * @date 2020/2/26 18:29
 */
public class AtomicIntegerAcid {

    private volatile static AtomicInteger num = new AtomicInteger();

    public static void  add(){
        num.getAndIncrement(); // 等價於 num++
    }

    public static void main(String[] args) {
        for (int i = 1; i <= 20; i++) {
            new Thread(()->{
                for (int j = 1; j <= 1000; j++) {
                    add();  // 20 * 1000 = 20000
                }
            },String.valueOf(i)).start();
        }

        // main線程等待上面執行完成,判斷線程存活數   2
        while (Thread.activeCount()>2){ // main  gc
            //線程放棄當前分得的 CPU 時間,但是不使線程阻塞
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName()+" "+num);
    }
}

17.3. volatile 禁止 指令重排講解

17.3.1. 什麼是指令重排

​ 計算機在執行程序之後,爲了提高性能,編譯器和處理器會進行指令重排!

​ 處理在指令重排的時候必須要考慮數據之間的依賴性!

指令重排:程序最終執行的代碼,不一定是按照你寫的順序來的!

int x = 11;  // 語句1
int y = 12;  // 語句2
x = y + 5;   // 語句3
y = x*x ;    // 語句4

//怎麼執行
1234
2134
1324
    
// 請問語句4 能在語句3前面執行嗎? 能

加深 : int x,y,a,b = 0;

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

假設編譯器進行了指令重排,就會出現如下效果!

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

package com.interview.concurrent.volatiles;

/**
 * @author yangxj
 * @description 描述:指令重排
 * 兩個線程交替執行的!
 * @date 2020/2/26 18:38
 */
public class InstructionReset {

    public static void main(String[] args) {
        InstructionWare instructionWare = new InstructionWare();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                instructionWare.m1();
            }
        },"A").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                instructionWare.m2();
            }
        },"B").start();
    }

}

class InstructionWare{
    int a = 0;
    boolean flag = false;

    public void m1(){  // A
        flag = true;   // 語句2
        a = 1;         // 語句1
    }

    public void m2(){  // B
        if (flag){
            a = a + 5;  // 語句3
            System.out.println("m2=>"+a);
        }
    }

}

由於有指令重排的問題,語句3可能在語句1先執行,這樣就導致最終結果a = 5而不是6!

volatite: 能實現禁止指令重排!

17.3.2. volatile實現禁止指令重排原理:內存屏障

volatile實現禁止指令重排原理:內存屏障

內存屏障: 作用於CPU的指令,主要作用兩個:

1、 保證特定的操作執行順序;

2、保證某些變量的內存可見性。

在這裏插入圖片描述

禁止指令重排,能保證線程的安全性

語句1先執行,這樣就導致最終結果a = 5而不是6!

volatite: 能實現禁止指令重排!

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