Volatile可見性分析(一)

MXC肖某某發表於2020-04-18
  • JUC(java.util.concurrent)
  • 程式和執行緒
  • 程式:後臺執行的程式(我們開啟的一個軟體,就是程式)
  • 執行緒:輕量級的程式,並且一個程式包含多個執行緒(同在一個軟體內,同時執行視窗,就是執行緒)
  • 併發和並行
  • 併發:同時訪問某個東西,就是併發
  • 並行:一起做某些事情,就是並行
  • JUC下的三個包
  • java.util.concurrent
  • java.util.concurrent.atomic
  • java.util.concurrent.locks

談談對Volatile的理解

Volatile在日常的單執行緒環境是應用不到的

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

JMM是什麼

JMM是Java記憶體模型,也就是Java Memory Model,簡稱JMM,本身是一種抽象的概念,實際上並不存在,它描述的是一組規則或規範,通過這組規範定義了程式中各個變數(包括例項欄位,靜態欄位和構成陣列物件的元素)的訪問方式

JMM關於同步的規定:

  • 執行緒解鎖前,必須把共享變數的值重新整理回主記憶體
  • 執行緒解鎖前,必須讀取主記憶體的最新值,到自己的工作記憶體
  • 加鎖和解鎖是同一把鎖

由於JVM執行程式的實體是執行緒,而每個執行緒建立時JVM都會為其建立一個工作記憶體(有些地方稱為棧空間),工作記憶體是每個執行緒的私有資料區域,而Java記憶體模型中規定所有變數都儲存在主記憶體,主記憶體是共享記憶體區域,所有執行緒都可以訪問,但執行緒對變數的操作(讀取賦值等)必須在工作記憶體中進行,首先要將變數從主記憶體拷貝到自己的工作記憶體空間,然後對變數進行操作,操作完成後再將變數寫會主記憶體,不能直接操作主記憶體中的變數,各個執行緒中的工作記憶體中儲存著主記憶體中的變數副本拷貝,因此不同的執行緒間無法訪問對方的工作記憶體,執行緒間的通訊(傳值)必須通過主記憶體來完成,其簡要訪問過程:

資料傳輸速率:硬碟 < 記憶體 < < cache < CPU

上面提到了兩個概念:主記憶體 和 工作記憶體

  • 主記憶體:就是計算機的記憶體,也就是經常提到的8G記憶體,16G記憶體

  • 工作記憶體:但我們例項化 new student,那麼 age = 25 也是儲存在主記憶體中

  • 當同時有三個執行緒同時訪問 student中的age變數時,那麼每個執行緒都會拷貝一份,到各自的工作記憶體,從而實現了變數的拷貝

即:JMM記憶體模型的可見性,指的是當主記憶體區域中的值被某個執行緒寫入更改後,其它執行緒會馬上知曉更改後的值,並重新得到更改後的值。

JMM的特性

JMM的三大特性,volatile只保證了兩個,即可見性和有序性,不滿足原子性

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

可見性程式碼驗證

但我們對於成員變數沒有新增任何修飾時,是無法感知其它執行緒修改後的值

package com.moxi.interview.study.thread;

/**
* Volatile Java虛擬機器提供的輕量級同步機制
*
* 可見性(及時通知)
* 不保證原子性
* 禁止指令重排
*
*/

import java.util.concurrent.TimeUnit;

/**
 * 假設是主實體記憶體
 */
class MyData {

    int number = 0;

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

/**
 * 驗證volatile的可見性
 * 1. 假設int number = 0, number變數之前沒有新增volatile關鍵字修飾
 */
public class VolatileDemo {

    public static void main(String args []) {

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

        // AAA執行緒 實現了Runnable介面的,lambda表示式
        new Thread(() -> {

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

            // 執行緒睡眠3秒,假設在進行運算
            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執行緒就一直在這裡等待迴圈,直到number的值不等於零
        }

        // 按道理這個值是不可能列印出來的,因為主執行緒執行的時候,number的值為0,所以一直在迴圈
        // 如果能輸出這句話,說明AAA執行緒在睡眠3秒後,更新的number的值,重新寫入到主記憶體,並被main執行緒感知到了
        System.out.println(Thread.currentThread().getName() + "\t mission is over");

        /**
         * 最後輸出結果:
         * AAA	 come in
         * AAA	 update number value:60
         * 最後執行緒沒有停止,並行沒有輸出  mission is over 這句話,說明沒有用volatile修飾的變數,是沒有可見性
         */

    }
}

輸出結果為

最後執行緒沒有停止,並行沒有輸出 mission is over 這句話,說明沒有用volatile修飾的變數,是沒有可見性

當我們修改MyData類中的成員變數時,並且新增volatile關鍵字修飾

/**
 * 假設是主實體記憶體
 */
class MyData {
    /**
     * volatile 修飾的關鍵字,是為了增加 主執行緒和執行緒之間的可見性,只要有一個執行緒修改了記憶體中的值,其它執行緒也能馬上感知
     */
    volatile int number = 0;

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

最後輸出的結果為:

主執行緒也執行完畢了,說明volatile修飾的變數,是具備JVM輕量級同步機制的,能夠感知其它執行緒的修改後的值。

相關文章