volatile如何防止指令重排序?原來使用了內存屏障

2020-12-17 愚公要移山1

在多線程的世界裡,一共有三個問題:原子性問題、可見性問題、有序性問題。整個java並發體系也是圍繞著如何解決這三個問題來設計的。volatile關鍵字也不例外,我們都知道它解決了可見性和有序性,但是不能保證原子性。這篇文章也主要基於其中一個特性,也就是研究一下volatile是如何保證有序性的。

一、有序性

1、有序性案例

有序性指的是:程序執行的順序按照代碼的先後順序執行。我們可以先看一個被列舉了一萬次的代碼:

按照我們自己常規的想法,順序應該從上往下依次執行,但是真實情況是:jvm會在真正執行這段代碼的時候進行優化,發生指令的重排序。因此不能保證語句1一定在語句2先執行。

2、數據依賴性

上面的例子,你還會發現這樣一個特點,就算是發生了指令的重排序,但是最後的結果總是正確的。我們再舉一個例子:

這種情況會發生指令重排序嗎?顯然不會,原因是處理器在進行重排序時是會考慮指令之間的數據依賴性,如果一個指令2必須用到指令1的結果,那麼處理器一定保證指令1在指令2執行。

3、多線程問題

這種數據的依賴性在單線程環境下一點問題沒有,因為總能保證數據的正確,但是在多線程環境下就會出現錯誤。我們再舉一個例子:

上面的這段代碼由於語句1和語句2沒有數據依賴性,因此會發生指令重排。do2隻要看到flag為true,就執行。因此可能的順序是:

(1)語句1先於語句2:語句2->語句3->語句1->語句4。這時候的結果i=1。

(1)語句2先於語句1:語句2->語句3->語句4->語句1。這時候的結果i=0。

現在我們可以看到在多線程環境下如果發生了指令的重排序,會對結果造成影響。

上面一開始提到過,volatile可以保證有序性,也就是可以防止指令重排序。那麼它是如何解決的呢?這就是內存屏障。因此我們從內存屏障講起。

二、內存屏障

1、什麼是內存屏障

內存屏障其實就是一個CPU指令,在硬體層面上來說可以扥為兩種:Load Barrier 和 Store Barrier即讀屏障和寫屏障。主要有兩個作用:

(1)阻止屏障兩側的指令重排序;

(2)強制把寫緩衝區/高速緩存中的髒數據等寫回主內存,讓緩存中相應的數據失效。

在JVM層面上來說作用與上面的一樣,但是種類可以分為四種:

2、volatile如何保證有序性?

首先一個變量被volatile關鍵字修飾之後有兩個作用:

(1)對於寫操作:對變量更改完之後,要立刻寫回到主存中。

(2)對於讀操作:對變量讀取的時候,要從主存中讀,而不是緩存。

OK,現在針對上面JVM的四種內存屏障,應用到volatile身上。因此volatile也帶有了這種效果。其實上面提到的這些內存屏障應用的效果,可以happen-before來總結歸納。

3、內存屏障分類

內存屏障有三種類型和一種偽類型:

(1)lfence:即讀屏障(Load Barrier),在讀指令前插入讀屏障,可以讓高速緩存中的數據失效,重新從主內存加載數據,以保證讀取的是最新的數據。

(2)sfence:即寫屏障(Store Barrier),在寫指令之後插入寫屏障,能讓寫入緩存的最新數據寫回到主內存,以保證寫入的數據立刻對其他線程可見。

(3)mfence,即全能屏障,具備ifence和sfence的能力。

(4)Lock前綴:Lock不是一種內存屏障,但是它能完成類似全能型內存屏障的功能。

為什麼說Lock是一種偽類型的內存屏障,是因為內存屏障具有happen-before的效果,而Lock在一定程度上保證了先後執行的順序,因此也叫做偽類型。比如,IO操作的指令,當指令不執行時,就具有了mfence的功能。

OK,一句話說完就是內存屏障保證了volatile的有序性。當然,我在知乎等很多平臺也看到了從計算機底層角度來分析的。還特地去看了看相關文獻。發現這裡面要是詳細寫,不是一兩篇就能完成的。

相關焦點

  • 從底層原理深度剖析volatile關鍵字,徹底徵服面試官
    本篇文章從底層原理層面深度剖析volatile關鍵字是如何實現內存可見性的,同時引入了Java內存模型、指令重排序以及內存屏障等知識點作為原理分析的知識支撐。指令重排序對並發編程安全性有很大影響,所以提供了一些happens-before規則定義一些禁止編譯優化的場景。
  • Java的synchronized 能防止指令重排序嗎?
    「面試官」:你有說道volatile關鍵字和synchronized關鍵字。synchronized可以保證原子性、有序性和可見性。而volatile卻只能保證有序性和可見性。那麼,我們再來看一下雙重校驗鎖實現的單例,已經使用了synchronized,為什麼還需要volatile?這個volatile是否可以去掉?「二胖:」 讓我想想,貌似好像確實可以去掉。
  • JMM、Volatile、重排序、happen-before原則
    JMM(Java Memory Model:java內存模型)這是一個java技術規範,java的強大之一是它的多線程支持。java多線程執行期間是如何使用內存的呢?JMM就是這樣一個規範,它描述了多線程執行時的內存使用方式。
  • Java程式設計師面試必備:Volatile全方位解析
    「指令重排序」了解一下,指令重排是指在程序執行過程中,「為了提高性能」, 「編譯器和CPU可能會對指令進行重新排序」。CPU重排序包括指令並行重排序和內存系統重排序,重排序類型和重排序執行過程如下:實際上,可以給flag加上volatile關鍵字,來保證有序性。當然,也可以通過synchronized和Lock來保證有序性。
  • java並發編程之volatile關鍵字
    >例如結果如果不設置stop變量為volatile,並不意味著程序一定不會結束因為這個stop變量的刷新操作也需要看cpu的處理方式但是加上volatile一定不會出現問題2、防止重排序但是由於作業系統可以對指令進行重排序,所以上面的過程也可能會變成如下過程:(1)分配內存空間。(2)將內存空間的地址賦值給對應的引用。
  • Java研發技術——Volatile原理詳解
    如何保證可見性#我們使用X86處理器下通過工具獲取JIT編譯器生成的彙編指令來查看對volatile進行寫操作會發生什麼Copyinstance = new Singleton();轉成彙編代碼如下Copy0x01a3de1d: movb $0×0,0×
  • Java 多線程 —— 深入理解 volatile 的原理以及應用
    happens-before是java內存模型向我們提供的內存可見性保證,這也就是我們第一個問題的解答,volatiel如何保證對共享變量同步的。 上述中我們講到volatile 中有一個特性,有序性,防止jvm對其重排序,那麼究竟是如何做的,我們看一下。重排序分為編譯器重排序和處理器重排序。為了實現volatile內存語義,jvm會分別限制這兩種類型的重排序類型。針對編譯器制定的volatile重排序規則:
  • 深入分析volatile是如何實現可見性和有序性的
    內存屏障可見性和有序性是基於各種內存屏障(禁止指令重排序)來實現的,先來看下有哪些內存屏障類型,以及可以解決那些因重排序引起的有序性問題。對有序性不太理解的同學可以先看下前這篇文章修復的變量,在它的讀/寫操作前後都會加上不同的內存屏障。
  • 面試官最愛的 volatile 關鍵字,這些問題你都搞懂了沒?
    可見性即用volatile關鍵字修飾的成員變量表明該變量不存在工作線程的副本,線程每次直接都從主內存中讀取,每次讀取的都是最新的值,這也就保證了變量對其他線程的可見性。另外,使用volatile還能確保變量不能被重排序,保證了有序性。
  • 知名公司面試題:談談你對volatile關鍵字的理解
    在Java多線程的開發中有三種特性:原子性可見性有序性volatile主要作用是保證內存可見性和防止指令重排序。也就是說,volatile關鍵字修飾的變量看到的隨時是自己的最新值。線程1中對變量v的最新修改,對線程2是可見的。防止指令重排序在基於偏序關係的Happens-Before內存模型中,指令重排技術大大提高了程序執行效率,但同時也引入了一些問題。
  • Java內存模型與volatile關鍵字
    Java的內存模型大概樣子還是有必要了解下的,今天就學習了下,順便學習了一點volatile關鍵字!每個線程獲取這類變量都是先把變量從主內存加載到工作內存,然後才能進行使用,同樣如果是修改,那麼也是先修改了工作內存中的變量,然後再從工作內存同步進主內存中。
  • 面試必問的volatile,你了解多少
    本來轉載自公眾號  佔小狼的博客作者佔小狼   來自 美團點評 基礎架構組前言Java中volatile這個熱門的關鍵字
  • 打工人,從 JMM 透析 volatile 與 synchronized 原理
    內存屏障:內存可見性與指令重排序那 JMM 如何保障指令重排序排序,內存可見性帶來並發訪問問題?內存屏障(Memory Barrier)用於控制在特定條件下的重排序和內存可見性問題。JMM 內存屏障可分為讀屏障和寫屏障,Java 的內存屏障實際上也是上述兩種的組合,完成一系列的屏障和數據同步功能。Java 編譯器在生成字節碼時,會在執行指令序列的適當位置插入內存屏障來限制處理器的重排序。
  • 再有人問你volatile是什麼,把這篇文章也發給他.
    通過禁止指令重排優化,就可以保證代碼程序會嚴格按照代碼的先後順序執行。那麼volatile又是如何禁止指令重排的呢?先給出結論:volatile是通過內存屏障來來禁止指令重排的。內存屏障(Memory Barrier)是一類同步屏障指令,是CPU或編譯器在對內存隨機訪問的操作中的一個同步點,使得此點之前的所有讀寫操作都執行後才可以開始執行此點之後的操作。下表描述了和volatile有關的指令重排禁止行為:從上表我們可以看出:當第二個操作是volatile寫時,不管第一個操作是什麼,都不能重排序。
  • 深度解析volatile關鍵字,就是這麼簡單
    和步驟4重排序,步驟4可能會和步驟1和步驟3重排序,但是步驟1、步驟3和步驟5之間不能重排序,步驟2、步驟4和步驟5之間不能重排序,因為它們之間存在依賴關係,一旦重排序,線程表現為串行的語義將無法得到保證。
  • 一文讀懂Java關鍵詞之volatile作用(內存屏障)
    我們還知道Java內存模型中,各個線程還保存了一份主內存中數據的拷貝,那麼不同線程的拷貝應該如何保證數據的一致性呢,今天我們就跟大家一起看看其中的一個技術。如果線程A更新了值,並且刷回了主內存,線程B在使用onWork之前是從主內存中讀取最新的值而不使用局部內存中的值,那麼這樣就肯定能夠避免上述問題了。
  • 從零開始了解多線程知識之開始篇目——jvm&volatile
    如果兩個操作的執行次序無法從happens-before原則推導出來,那麼它們就不能保證它們的有序性,虛擬機可以隨意地對它們進行重排序。指令重排序:java語言規範規定JVM線程內部維持順序化語義。即只要程序的最終結果與它順序化情況的結果相等,那麼指令的執行順序可以與代碼順序不一致,此過程叫指令的重排序。指令重排序的意義是什麼?
  • 可惜了,面試敗在了volatile關鍵字上,直擊痛點搞懂volatile
    (實現可見性)禁止進行指令重排序。(實現有序性)volatile 只能保證對單次讀/寫的原子性。i++ 這種操作不能保證原子性。 }}流程圖大致是這樣的:volatile 指令重排volatile 變量的內存可見性是基於內存屏障(Memory Barrier)實現。關於內存屏障的具體講解以前寫過不再重複,JMM裝逼於無形這裡說過。
  • volatile你以為你真的懂?
    volatile你想通吃各種企業的面試官,你只需要掌握以下這兩大點,也就是Volatile的特性:保證線程的可見性MESI禁止指令重排序答案是要加的,我們測試很那出現讓上面的代碼出錯的情況,所以很多人寫代碼不加這個Volatile也不會出現問題,但是不加Volatile問題會處在指令重排序上。指令重排序知多少再看上面雙重檢查的代碼Demo,「看起來」非常完美:既減少了阻塞,又避免了競態條件。
  • 深入synchronized和volatile底層原理
    指令重排不太方便觀測,這裡記一筆編譯器和處理器都會進行指令重排。內存屏障前面說了緩存一致性問題,多線程下的指令重排序問題,於是CPU廠商就搞了個叫內存屏障的東西來解決問題,不同的硬體平臺實現內存屏障的手段並不是一樣。