1. 程式人生 > >【死磕Java併發】-----Java記憶體模型之分析volatile

【死磕Java併發】-----Java記憶體模型之分析volatile

  1. volatile可見性;對一個volatile的讀,總可以看到對這個變數最終的寫;
  2. volatile原子性;volatile對單個讀/寫具有原子性(32位Long、Double),但是複合操作除外,例如i++;
  3. JVM底層採用“記憶體屏障”來實現volatile語義

下面LZ就通過happens-before原則和volatile的記憶體語義兩個方向介紹volatile。

volatile與happens-before

在這篇部落格【死磕Java併發】—–Java記憶體模型之happend-before中LZ闡述了happens-before是用來判斷是否存資料競爭、執行緒是否安全的主要依據,它保證了多執行緒環境下的可見性。下面我們就那個經典的例子來分析volatile變數的讀寫建立的happens-before關係。

public class VolatileTest {

    int i = 0;
    volatile boolean flag = false;

    //Thread A
    public void write(){
        i = 2;              //1
        flag = true;        //2
    }

    //Thread B
    public void read(){
        if(flag){                                   //3
            System.out.println("---i = "
+ i); //4 } } }

依據happens-before原則,就上面程式得到如下關係:

  • 依據happens-before程式順序原則:1 happens-before 2、3 happens-before 4;
  • 根據happens-before的volatile原則:2 happens-before 3;
  • 根據happens-before的傳遞性:1 happens-before 4

操作1、操作4存在happens-before關係,那麼1一定是對4可見的。可能有同學就會問,操作1、操作2可能會發生重排序啊,會嗎?如果看過LZ的部落格就會明白,volatile除了保證可見性外,還有就是禁止重排序。所以A執行緒在寫volatile變數之前所有可見的共享變數,線上程B讀同一個volatile變數後,將立即變得對執行緒B可見。

volataile的記憶體語義及其實現

在JMM中,執行緒之間的通訊採用共享記憶體來實現的。volatile的記憶體語義是:

當寫一個volatile變數時,JMM會把該執行緒對應的本地記憶體中的共享變數值立即重新整理到主記憶體中。
當讀一個volatile變數時,JMM會把該執行緒對應的本地記憶體設定為無效,直接從主記憶體中讀取共享變數

所以volatile的寫記憶體語義是直接重新整理到主記憶體中,讀的記憶體語義是直接從主記憶體中讀取。
那麼volatile的記憶體語義是如何實現的呢?對於一般的變數則會被重排序,而對於volatile則不能,這樣會影響其記憶體語義,所以為了實現volatile的記憶體語義JMM會限制重排序。其重排序規則如下:

翻譯如下:

  1. 如果第一個操作為volatile讀,則不管第二個操作是啥,都不能重排序。這個操作確保volatile讀之後的操作不會被編譯器重排序到volatile讀之前;
  2. 當第二個操作為volatile寫是,則不管第一個操作是啥,都不能重排序。這個操作確保volatile寫之前的操作不會被編譯器重排序到volatile寫之後;
  3. 當第一個操作volatile寫,第二操作為volatile讀時,不能重排序。

volatile的底層實現是通過插入記憶體屏障,但是對於編譯器來說,發現一個最優佈置來最小化插入記憶體屏障的總數幾乎是不可能的,所以,JMM採用了保守策略。如下:

  • 在每一個volatile寫操作前面插入一個StoreStore屏障
  • 在每一個volatile寫操作後面插入一個StoreLoad屏障
  • 在每一個volatile讀操作後面插入一個LoadLoad屏障
  • 在每一個volatile讀操作後面插入一個LoadStore屏障

StoreStore屏障可以保證在volatile寫之前,其前面的所有普通寫操作都已經重新整理到主記憶體中。

StoreLoad屏障的作用是避免volatile寫與後面可能有的volatile讀/寫操作重排序。

LoadLoad屏障用來禁止處理器把上面的volatile讀與下面的普通讀重排序。

LoadStore屏障用來禁止處理器把上面的volatile讀與下面的普通寫重排序。

下面我們就上面那個VolatileTest例子分析下:

public class VolatileTest {
    int i = 0;
    volatile boolean flag = false;
    public void write(){
        i = 2;
        flag = true;
    }

    public void read(){
        if(flag){
            System.out.println("---i = " + i); 
        }
    }
}

這裡寫圖片描述

上面通過一個例子稍微演示了volatile指令的記憶體屏障圖例。

volatile的記憶體屏障插入策略非常保守,其實在實際中,只要不改變volatile寫-讀得記憶體語義,編譯器可以根據具體情況優化,省略不必要的屏障。如下(摘自方騰飛 《Java併發程式設計的藝術》):

public class VolatileBarrierExample {
    int a = 0;
    volatile int v1 = 1;
    volatile int v2 = 2;

    void readAndWrite(){
        int i = v1;     //volatile讀
        int j = v2;     //volatile讀
        a = i + j;      //普通讀
        v1 = i + 1;     //volatile寫
        v2 = j * 2;     //volatile寫
    }
}

沒有優化的示例圖如下:

這裡寫圖片描述

我們來分析上圖有哪些記憶體屏障指令是多餘的

1:這個肯定要保留了

2:禁止下面所有的普通寫與上面的volatile讀重排序,但是由於存在第二個volatile讀,那個普通的讀根本無法越過第二個volatile讀。所以可以省略。

3:下面已經不存在普通讀了,可以省略。

4:保留

5:保留

6:下面跟著一個volatile寫,所以可以省略

7:保留

8:保留

所以2、3、6可以省略,其示意圖如下:

這裡寫圖片描述

參考資料

  1. 方騰飛:《Java併發程式設計的藝術》

相關推薦

Java併發-----Java記憶體模型分析volatile

volatile可見性;對一個volatile的讀,總可以看到對這個變數最終的寫; volatile原子性;volatile對單個讀/寫具有原子性(32位Long、Double),但是複合操作除外,例如i++; JVM底層採用“記憶體屏障”來實現volat

Java併發Java記憶體模型分析volatile

本文轉載自公眾號: Java技術驛站volatile可見性;對一個volatile的讀,總可以看

Java併發-----Java記憶體模型happens-before

在上篇部落格(【死磕Java併發】—–深入分析volatile的實現原理)LZ提到過由於存線上程本地記憶體和主記憶體的原因,再加上重排序,會導致多執行緒環境下存在可見性的問題。那麼我們正確使用同步、鎖的情況下,執行緒A修改了變數a何時對執行緒B可見? 我們無法就所有場景來規

Java併發--Java記憶體模型happens-before

在上篇部落格(【死磕Java併發】—–深入分析volatile的實現原理)LZ提到過由於存線上程本地記憶體和主記憶體的原因,再加上重排序,會導致多執行緒環境下存在可見性的問題。那麼我們正確使用同步、鎖的情況下,執行緒A修改了變數a何時對執行緒B可見?我們無法就所有場景來規定某

Java併發-----Java記憶體模型總結

經過四篇部落格闡述,我相信各位對Java記憶體模型有了最基本認識了,下面LZ就做一個比較簡單的總結。 總結 JMM規定了執行緒的工作記憶體和主記憶體的互動關係,以及執行緒之間的可見性和程式的執行順序。一方面,要為程式設計師提供足夠強的記憶體可見性保證;另

Java併發-----J.U.CAQS:阻塞和喚醒執行緒

此篇部落格所有原始碼均來自JDK 1.8 線上程獲取同步狀態時如果獲取失敗,則加入CLH同步佇列,通過通過自旋的方式不斷獲取同步狀態,但是在自旋的過程中則需要判斷當前執行緒是否需要阻塞,其主要方法在acquireQueued(): if (sho

Java併發-----J.U.C阻塞佇列:ArrayBlockingQueue

ArrayBlockingQueue,一個由陣列實現的有界阻塞佇列。該佇列採用FIFO的原則對元素進行排序新增的。 ArrayBlockingQueue為有界且固定,其大小在構造時由建構函式來決定,確認之後就不能再改變了。ArrayBlockingQueu

Java併發—– J.U.C併發工具類:Semaphore

此篇部落格所有原始碼均來自JDK 1.8訊號量Semaphore是一個控制訪問多個共享資源的計數

Java併發—– J.U.CAQS:同步狀態的獲取與釋放

此篇部落格所有原始碼均來自JDK 1.8在前面提到過,AQS是構建Java同步元件的基礎,我們期

Java併發-----J.U.C併發工具類:Exchanger

此篇部落格所有原始碼均來自JDK 1.8 前面三篇部落格分別介紹了CyclicBarrier、CountDownLatch、Semaphore,現在介紹併發工具類中的最後一個Exchange。Exchange是最簡單的也是最複雜的,簡單在於API非常簡

Java併發-----J.U.CCondition

此篇部落格所有原始碼均來自JDK 1.8 在沒有Lock之前,我們使用synchronized來控制同步,配合Object的wait()、notify()系列方法可以實現等待/通知模式。在Java SE5後,Java提供了Lock介面,相對於Synch

Java併發-----J.U.C重入鎖:ReentrantLock

此篇部落格所有原始碼均來自JDK 1.8 ReentrantLock,可重入鎖,是一種遞迴無阻塞的同步機制。它可以等同於synchronized的使用,但是ReentrantLock提供了比synchronized更強大、靈活的鎖機制,可以減少死鎖發生

Java併發-----J.U.C阻塞佇列:DelayQueue

DelayQueue是一個支援延時獲取元素的無界阻塞佇列。裡面的元素全部都是“可延期”的元素,列頭的元素是最先“到期”的元素,如果佇列裡面沒有元素到期,是不能從列頭獲取元素的,哪怕有元素也不行。也就是說只有在延遲期到時才能夠從佇列中取元素。 DelayQu

Java併發-----J.U.C併發工具類:CyclicBarrier

此篇部落格所有原始碼均來自JDK 1.8 CyclicBarrier,一個同步輔助類,在API中是這麼介紹的: 它允許一組執行緒互相等待,直到到達某個公共屏障點 (common barrier point)。在涉及一組固定大小的執行緒的程式中,這些執

Java併發—–J.U.CAQS(一篇就夠了)

作者:大明哥  原文地址:http://cmsblogs.com 越是核心的東西越是要反覆看,本文篇幅較長,希望各位細細品讀,來回多讀幾遍理解下。 AQS簡介 java的內建鎖一直都是備受爭議的,在JDK 1.6之前,synchronized這個重量級鎖其效能一直都

Java併發-----J.U.CAQS:AQS簡介

Java的內建鎖一直都是備受爭議的,在JDK 1.6之前,synchronized這個重量級鎖其效能一直都是較為低下,雖然在1.6後,進行大量的鎖優化策略(【死磕Java併發】—–深入分析synchronized的實現原理),但是與Lock相比synchroni

Java併發-----J.U.CAQS:CLH同步佇列

此篇部落格所有原始碼均來自JDK 1.8 CLH同步佇列是一個FIFO雙向佇列,AQS依賴它來完成同步狀態的管理,當前執行緒如果獲取同步狀態失敗時,AQS則會將當前執行緒已經等待狀態等資訊構造成一個節點(Node)並將其加入到CLH同步佇列,同時會

Java併發Java中的原子操作

Java中的原子操作 原子更新基本型別 原子更新陣列 原子更新引用型別 原子更新欄位類 參考 原子更新基本型別 一個生動的例子 public class AtomicIntegerExample { privat

Java併發Java中的執行緒池

Java中的執行緒池 執行流程 執行緒池的建立 提交任務 關閉執行緒池 參考 執行流程 處理流程如下: execute()方法執行示意圖如下: 執行緒池的建立 corePoolSize:執行緒池

springboot2.0springboot基於web開發

宣告,使用 maven3.5.4,springboot2.0,JDK8 ,idea2018.2 模組目錄結構: main 主方法: @SpringBootApplication public class WebApplication { public static voi