Volatile可見性分析(一)


  • 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輕量級同步機制的,能夠感知其它線程的修改后的值。

Volatile緩存可見性實現原理

底層實現主要是通過匯編lock前綴指令,它會鎖定這塊內存區域的緩存(緩存行鎖定)並寫回到主內存。IA-32和Intel 64架構軟件開發者手冊對lock指令的解釋:

1)會將當前處理器緩存行的數據立即寫回到系統內存。
2)這個寫回的操作會引起在其他CPU里緩存了該內存地址的數據無效(MESI協議)
3)提供內存屏障功能,使lock前后指令不能重排序

  • read(讀取):從主內存讀取數據
  • load(載入):將主內存讀取到的數據寫入工作內存
  • use(使用) :從工作內存讀取數據來計算
  • assign(賦值):將計算好的值重新賦值到工作內存中
  • store(存儲):將工作內存數據寫入主內存
  • write(寫入):將store過去的變量值賦值給主內存中的變量
  • lock(鎖定) :將主內存變量加鎖,標識為線程獨占狀態
  • unlock(解鎖):將主內存變量解鎖,解鎖后其他線程可以鎖定該變量


免責聲明!

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



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