1. 程式人生 > >volatile 手摸手帶你解析

volatile 手摸手帶你解析

前言

volatile 是 Java 裡的一個重要的指令,它是由 Java 虛擬機器裡提供的一個輕量級的同步機制。一個共享變數宣告為 volatile 後,特別是在多執行緒操作時,正確使用 volatile 變數,就要掌握好其原理。

特性

volatile 具有可見性和有序性的特性,同時,對 volatile 修飾的變數進行單個讀寫操作是具有原子性。

這幾個特性到底是什麼意思呢?

  • 可見性: 當一個執行緒更新了 volatile 修飾的共享變數,那麼任意其他執行緒都能知道這個變數最後修改的值。簡單的說,就是多執行緒執行時,一個執行緒修改 volatile 共享變數後,其他執行緒獲取值時,一定都是這個修改後的值。
  • 有序性: 一個執行緒中的操作,相對於自身,都是有序的,Java 記憶體模型會限制編譯器重排序和處理器重排序。意思就會說 volatile 記憶體語義單個執行緒中是序列的語義。
  • 原子性: 多執行緒操作中,非複合操作單個 volatile 的讀寫是具有原子性的。

可見性

可見性是在多執行緒中保證共享變數的資料有效,接下來我們通過有 volatile 修飾的變數和無 volatile 修飾的變數程式碼的執行結果來做對比分析。

無 volatile 修飾變數

以下是沒有 volatile 修飾變數程式碼,通過建立兩個執行緒,來驗證 flag 被其中一個執行緒修改後的執行情況。

/**
 * Created by YANGTAO on 2020/3/15 0015.
 */
public class ValatileDemo {

    static Boolean flag = true;

    public static void main(String[] args) {

        // A 執行緒,判斷其他執行緒修改 flag 之後,資料是否對本執行緒有效
        new Thread(() -> {
            while (flag) {

            }
            System.out.printf("********** %s 執行緒執行結束! **********", Thread.currentThread().getName());
        }, "A").start();

        
        // B 執行緒,修改 flag 值
        new Thread(() -> {
            try {
                // 避免 B 執行緒比 A 執行緒先執行修改 flag 值  
                TimeUnit.SECONDS.sleep(1);
                flag = false;
                // 如果 flag 值修改後,讓 B 執行緒先列印資訊
                TimeUnit.SECONDS.sleep(2);

                System.out.printf("********** %s 執行緒執行結束! **********", Thread.currentThread().getName());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "B").start();

    }
}

上面程式碼中,當 flag 初始值 true,被 B 執行緒修改為 false。如果修改後的值對 A 執行緒有效,那麼正常情況下 A 執行緒會先於 B 執行緒結束。執行結果如下:

執行結果是:當 B 執行緒執行結束後,flag = false並未對 A 執行緒生效,A 執行緒死迴圈。

volatile 修飾變數

在上述程式碼中,當我們把 flag 使用 volatile 修飾:

/**
 * Created by YANGTAO on 2020/3/15 0015.
 */
public class ValatileDemo {

    static volatile Boolean flag = true;

    public static void main(String[] args) {

        // A 執行緒,判斷其他執行緒修改 flag 之後,資料是否對本執行緒有效
        new Thread(() -> {
            while (flag) {

            }
            System.out.printf("********** %s 執行緒執行結束! **********", Thread.currentThread().getName());
        }, "A").start();

        
        // B 執行緒,修改 flag 值
        new Thread(() -> {
            try {
                // 避免 B 執行緒比 A 執行緒先執行修改 flag 值  
                TimeUnit.SECONDS.sleep(1);
                flag = false;
                // 如果 flag 值修改後,讓 B 執行緒先列印資訊
                TimeUnit.SECONDS.sleep(2);

                System.out.printf("********** %s 執行緒執行結束! **********", Thread.currentThread().getName());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "B").start();

    }
}

執行結果:

B 執行緒修改 flag 值後,對 A 執行緒資料有效,A 執行緒跳出迴圈,執行完成。所以 volatile 修飾的變數,有新值寫入後,對其他執行緒來說,資料是有效的,能被其他執行緒讀到。

主記憶體和工作記憶體

上面程式碼中的變數加了 volatile 修飾,為什麼就能被其他執行緒讀取到,這就涉及到 Java 記憶體模型規定的變數訪問規則。

  • 主記憶體:主記憶體是機器硬體的記憶體,主要對應Java 堆中的物件例項資料部分。
  • 工作記憶體:每個執行緒都有自己的工作記憶體,對應虛擬機器棧中的部分割槽域,執行緒對變數的讀/寫操作都必須在工作記憶體中進行,不能直接讀寫主記憶體的變數。

上面無 volatile 修飾變數部分的程式碼執行示意圖如下:

當 A 執行緒讀取到 flag 的初始值為true,進行 while 迴圈操作,B 執行緒將工作記憶體 B 裡的 flag 更新為false,然後將值傳送到主記憶體進行更新。隨後,由於此時的 A 執行緒不會主動重新整理主記憶體中的值到工作記憶體 A 中,所以執行緒 A 所取得 flag 值一直都是true,A 執行緒也就為死迴圈不會停止下來。

上面volatile 修飾變數部分的程式碼執行示意圖如下:

當 B 執行緒更新 volatile 修飾的變數時,會向 A 執行緒通過執行緒之間的通訊傳送通知(JDK5 或更高版本),並且將工作記憶體 B 中更新的值同步到主記憶體中。A 執行緒接收到通知後,不會再讀取工作記憶體 A 中的值,會將主記憶體的變數通過主記憶體和工作記憶體之間的互動協議,拷貝到工作記憶體 A 中,這時讀取的值就是執行緒 A 更新後的值flag = false
整個變數值得傳遞過程中,執行緒之間不能直接訪問自身以外的工作記憶體,必須通過主記憶體作為中轉站傳遞變數值。在這傳遞過程中是存在拷貝操作的,但是物件的引用,虛擬機器不會整個物件進行拷貝,會存線上程訪問的欄位拷貝。

有序性

volatile 包含禁止指令重排的語義,Java 記憶體模型會限制編譯器重排序和處理器重排序,簡而言之就是單個執行緒內表現為序列語義。
那什麼是重排序?
重排序的目的是編譯器和處理器為了優化程式效能而對指令序列進行重排序,但在單執行緒和單處理器中,重排序不會改變有資料依賴關係的兩個操作順序。
比如:

/**
 * Created by YANGTAO on 2020/3/15 0015.
 */
public class ReorderDemo {
    
    static int a = 0;

    static int b = 0;

    public static void main(String[] args) {
        a = 2;
        b = 3;
    }
}

// 重排序後:

public class ReorderDemo {
    
    static int a = 0;

    static int b = 0;

    public static void main(String[] args) {
        b = 3;  // a 和 b 重排序後,調換了位置
        a = 2;
    }
}

但是如果在單核處理器和單執行緒中資料之間存在依賴關係則不會進行重排序,比如:

/**
 * Created by YANGTAO on 2020/3/15 0015.
 */
public class ReorderDemo {

    static int a = 0;

    static int b = 0;

    public static void main(String[] args) {
        a = 2;
        b = a;
    }
}

// 由於 a 和 b 存在資料依賴關係,則不會進行重排序

volatile 實現特有的記憶體語義,Java 記憶體模型定義以下規則(表格中的 No 代表不可以重排序):

Java 記憶體模型在指令序列中插入記憶體屏障來處理 volatile 重排序規則,策略如下:

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

該四種屏障意義:

  • StoreStore:在該屏障後的寫操作執行之前,保證該屏障前的寫操作已重新整理到主記憶體。
  • StoreLoad:在該屏障後的讀取操作執行之前,保證該屏障前的寫操作已重新整理到主記憶體。
  • LoadLoad:在該屏障後的讀取操作執行之前,保證該屏障前的讀操作已讀取完畢。
  • LoadStore:在該屏障後的寫操作執行之前,保證該屏障前的讀操作已讀取完畢。

原子性

前面有提到 volatile 的原子性是相對於單個 volatile 變數的讀/寫具有,比如下面程式碼:

/**
 * Created by YANGTAO on 2020/3/15 0015.
 */
public class AtomicDemo {

    static volatile int num = 0;

    public static void main(String[] args) throws InterruptedException {

        final CountDownLatch latch = new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {  // 建立 10 個執行緒
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {    // 每個執行緒累加 1000
                    num ++;
                }
                latch.countDown();
            }, String.valueOf(i+1)).start();
        }

        latch.await();
        
        // 所有執行緒累加計算的資料
        System.out.printf("num: %d", num);
    }
}

上面程式碼中,如果 volatile 修飾 num,在 num++ 運算中能持有原子性,那麼根據以上數量的累加,最後應該是 num: 10000
程式碼執行結果:

結果與我們預計資料的相差挺多,雖然 volatile 變數在更新值的時候回通知其他執行緒重新整理主記憶體中最新資料,但這隻能保證其基本型別變數讀/寫的原子操作(如:num = 2)。由於num++是屬於一個非原子操作的複合操作,所以不能保證其原子性。

使用場景

  1. volatile 變數最後的運算結果不依賴變數的當前值,也就是前面提到的直接賦值變數的原子操作,比如:儲存資料遍歷的特定條件的一個值。
  2. 可以進行狀態標記,比如:是否初始化,是否停止等等。

總結

volatile 是一個簡單又輕量級的同步機制,但在使用過程中,侷限性比較大,要想使用好它,必須瞭解其原理及本質,所以在使用過程中遇到的問題,相比於其他同步機制來說,更容易出現問題。但使用好 volatile,在某些解決問題上能獲取更佳的效能。

個人部落格: https://ytao.top
關注公眾號 【ytao】,更多原創好文

相關推薦

volatile 解析

前言 volatile 是 Java 裡的一個重要的指令,它是由 Java 虛擬機器裡提供的一個輕量級的同步機制。一個共享變數宣告為 volatile 後,特別是在多執行緒操作時,正確使用 volatile 變數,就要掌握好其原理。 特性 volatile 具有可見性和有序性的特性,同時,對 volati

【轉】用vue擼後臺 系列二(登錄權限篇)

userinfo ogr abort 變化 再次 狀態碼 quest -o 監聽 前言 拖更有點嚴重,過了半個月才寫了第二篇教程。無奈自己是一個業務猿,每天被我司的產品虐的死去活來,之前又病了一下休息了幾天,大家見諒。 進入正題,做後臺項目區別於做其它的項目,權限驗證與

部署git伺服器實現自動釋出程式碼

前段趁著活動買了個騰訊雲伺服器,一直沒抽時間去折騰,最近才開始部署了一個自己專屬的git倉庫,實現程式碼自動釋出部署功能。(我也想成為努力向上,積極進取的有志青年啊!)文章的內容基本都是參考自谷歌搜尋,然後按我的思路重新整理一遍,既是為了分享也是為了鞏固知識。 我也是第一次折騰伺服器,前期部署就

實現程式碼自動部署

為什麼? 為什麼要實現自動部署? 在2個月的時間裡,一直都在忙著整理部落格,每一個程式設計師都有一個部落格夢(當然也不排除有些是沒有的),我先後使用過各種部落格系統: vuepress react-static jekyll hexo ... 這些都因為前前後後的原因,

玩轉Xcode Extensions

題圖:from imgur 事情是這樣的前段時間看到,E神寫了個新玩具叫Code Friend很酷炫,私下裡給E神說拿來推廣讓iOSTips的讀者體驗一番,E神說目前功能還不完善,等再牛逼一點再拿來分享,然後就有了接下來的分享。 有沒有這樣的體驗,我們經常要開啟終端,跳轉到當前工程目錄,

用vue擼後臺 系列二(登入許可權篇)

前言 拖更有點嚴重,過了半個月才寫了第二篇教程。無奈自己是一個業務猿,每天被我司的產品虐的死去活來,之前又病了一下休息了幾天,大家見諒。 進入正題,做後臺專案區別於做其它的專案,許可權驗證與安全性是非常重要的,可以說是一個後臺專案一開始就必須考慮和搭建的基礎核心功能

用合理的姿勢使用 webpack 4(上)

(點選上方公眾號,可快速關注)作者:華爾街見聞技術團隊 - 花褲衩segmentfault.co

用vue實現後臺管理許可權系統及頂欄三級選單顯示

手摸手,帶你用vue實現後臺管理許可權系統及頂欄三級選單顯示 效果演示地址 專案demo展示 重要功能總結 許可權功能的實現 許可權路由思路: 根據使用者登入的roles資訊與路由中配置的roles資訊進行比較過濾,生成可以訪問的路由表,並通過router.addRoutes(store.gett

圖解AQS的設計與實現,實現一把互斥鎖!

AQS是併發程式設計中非常重要的概念,它是juc包下的許多併發工具類,如CountdownLatch,CyclicBarrier,Semaphore 和鎖, 如ReentrantLock, ReaderWriterLock的實現基礎,提供了一個基於int狀態碼和佇列來實現的併發框架。本文將對AQS框架的幾個重

淺談Java中的Condition條件佇列,實現一個阻塞佇列!

條件佇列是什麼?可能很多人和我一樣答不出來,不過今天終於搞清楚了! 什麼是條件佇列 條件佇列:當某個執行緒呼叫了wait方法,或者通過Condition物件呼叫了await相關方法,執行緒就會進入阻塞狀態,並加入到對應條件佇列中。 在等待喚醒機制相關文章中我們提到了條件佇列,即當物件獲取到同步鎖之後,如果呼叫

理解Vue響應式原理

## 前言 響應式原理作為 `Vue` 的核心,使用資料劫持實現資料驅動檢視。在面試中是經常考查的知識點,也是面試加分項。 本文將會循序漸進的解析響應式原理的工作流程,主要以下面結構進行: 1. 分析主要成員,瞭解它們有助於理解流程 2. 將流程拆分,理解其中的作用 3. 結合以上的點,理解整體流程

理解Vue的Computed原理

## 前言 `computed` 在 `Vue` 中是很常用的屬性配置,它能夠隨著依賴屬性的變化而變化,為我們帶來很大便利。那麼本文就來帶大家全面理解 `computed` 的內部原理以及工作流程。 在這之前,希望你能夠對響應式原理有一些理解,因為 `computed` 是基於響應式原理進行工作。如果你對

理解Vue的Watch原理

## 前言 `watch` 是由使用者定義的資料監聽,當監聽的屬性發生改變就會觸發回撥,這項配置在業務中是很常用。在面試時,也是必問知識點,一般會用作和 `computed` 進行比較。 那麼本文就來帶大家從原始碼理解 `watch` 的工作流程,以及依賴收集和深度監聽的實現。在此之前,希望你能對響應式

YApi——在Win10環境下安裝YApi視覺化介面管理平臺

手摸手,帶你在Win10環境下安裝YApi視覺化介面管理平臺 YApi   YApi 是高效、易用、功能強大的 api 管理平臺,旨在為開發、產品、測試人員提供更優雅的介面管理服務。可以幫助開發者輕鬆建立、釋出、維護 API,YApi 還為使用者提供了優秀的互

搭建前後端分離商城系統】01 搭建基本程式碼框架、生成一個基本API

## 【手摸手,帶你搭建前後端分離商城系統】01 搭建基本程式碼框架、生成一個基本API 通過本教程的學習,將帶你從零搭建一個商城系統。 當然,這個商城涵蓋了很多流行的`知識點`和`技術核心` ### 我可以學習到什麼? - SpringBoot - 鑑權與認證、token、有關許可權的相關的內容

搭建前後端分離商城系統】02 VUE-CLI 腳手架生成基本專案,axios配置請求、解決跨域問題

## 【手摸手,帶你搭建前後端分離商城系統】02 VUE-CLI 腳手架生成基本專案,axios配置請求、解決跨域問題。 回顧一下上一節我們學習到的內容。已經將一個 `usm_admin 後臺使用者` 表的基本增刪改查全部都完成了。並且通過`swagger` 測試了我們的介面資訊,並且順利通過測試。本節將通

搭建前後端分離商城系統】03 整合Spring Security token 實現方案,完成主業務登入

## 【手摸手,帶你搭建前後端分離商城系統】03 整合Spring Security token 實現方案,完成主業務登入 上節裡面,我們已經將基本的前端 `VUE + Element UI` 整合到了一起。並且通過 `axios` 傳送請求到後端API。 解決跨域問題後、成功從後端獲取到資料。 本小結

學CSS

> **好好學習,天天向上** > > 本文已收錄至我的Github倉庫[**DayDayUP**](https://github.com/RobodLee/DayDayUP):github.com/RobodLee/DayDayUP,歡迎Star [HTML常用標籤總結](https:/

用Hexo擼部落格(二)之配置主題

[原文地址](https://blog.limeichao.cn/article/fdc79fa4.html) 在上一篇部落格[手摸手帶你用Hexo擼部落格(一)](https://www.cnblogs.com/big0range/p/14204956.html)中主要介紹了部落格的初步搭建 今天我們繼續講

用Hexo擼部落格(三)之新增評論系統

[原文地址](https://boke.limeichao.cn/article/7885e224.html) 注: 筆者採用的是butterfly主題, 主題內建整合評論系統 ## butterfly主題開啟評論 開啟評論需要在comments-use中填寫你需要的評論。 以Valine為例 ``