1. 程式人生 > >Java記憶體模型相關原則詳解

Java記憶體模型相關原則詳解

在《Java記憶體模型(JMM)詳解》一文中我們已經講到了Java記憶體模型的基本結構以及相關操作和規則。而Java記憶體模型又是圍繞著在併發過程中如何處理原子性、可見性以及有序性這三個特徵來構建的。本篇文章就帶大家瞭解一下相關概念、原則等內容。

原子性

原子性即一個操作或一系列是不可中斷的。即使是在多個執行緒的情況下,操作一旦開始,就不會被其他執行緒干擾。

比如,對於一個靜態變數int x兩條執行緒同時對其賦值,執行緒A賦值為1,而執行緒B賦值為2,不管執行緒如何執行,最終x的值要麼是1,要麼是2,執行緒A和執行緒B間的操作是沒有干擾的,這就是原子性操作,不可被中斷的。

Java記憶體模型對以下操作保證其原子性:read,load,assign,use,store,write。我們可以大致認為基本資料型別的訪問讀寫是具備原子性的(前面也提到了long和double型別的“半個變數”情況,不過幾乎不會發生)。

從Java記憶體模型底層來看有上面的原子性操作,但針對使用者來說,也就是我們編寫Java的程式,如果需要更大範圍的原子性保障,就需要同步關鍵字——synchronized來保障了。也就是說synchronized中的操作也具有原子性。

可見性

可見性是指當一個執行緒修改了共享變數的值,其他執行緒能夠立即得知這個修改。

Java記憶體模型是通過變數修改後將新值同步回主記憶體,在變數讀取前從主記憶體重新整理變數值,將主記憶體作為傳遞媒介。可回顧一下上篇文章的圖。

無論普通變數還是volatile變數都是如此,只不過volatile變數保證新值能夠立馬同步到主記憶體,使用時也立即從主記憶體重新整理,保證了多執行緒操作時變數的可見性。而普通變數不能夠保證。

除了volatile,synchronized和final也能夠實現可見性。

synchronized實現的可見性是通過“對一個變數執行unlock操作之前,必須先把此變數同步回主記憶體中”來保證的。

主要有兩個原則:執行緒解鎖前,必須把共享變數的最新值重新整理到主記憶體中;執行緒加鎖時,將清空工作記憶體中共享變數的值,從而使用共享變數時需要從主記憶體中重新讀取最新的值。

final的可見性是指:被final修飾的欄位在構造器中一旦初始化完成,並且構造器沒有把“this”的引用傳遞出去,那在其他執行緒中就能看見final的值。

有序性

在Java記憶體模型中有序性可歸納為這樣一句話:如果在本執行緒內觀察,所有操作都是有序的,如果在一個執行緒中觀察另一個執行緒,所有操作都是無序的。

有序性是指對於單執行緒的執行程式碼,執行是按順序依次進行的。但在多執行緒環境中,則可能出現亂序現象,因為在編譯過程會出現“指令重排”,重排後的指令與原指令的順序未必一致。

因此,上面歸納的前半句指的是執行緒內保證序列語義執行,後半句則指指“令重排現”象和“工作記憶體與主記憶體同步延遲”現象。

同樣,Java語言提供了volatile和synchronized兩個關鍵字來保證執行緒之間操作的有序性。

指令重排

計算機執行指令經過編譯之後形成指令序列。一般情況,指令序列是會輸出確定的結果,且每一次的執行都有確定的結果。

CPU和編譯器為了提升程式執行的效率,會按照一定的規則允許進行指令優化。但程式碼邏輯之間是存在一定的先後順序,併發執行時按照不同的執行邏輯會得到不同的結果。

  • 編譯器優化重排序:編譯器在不改變單執行緒程式語義的前提下,重新安排語句執行順序。
  • 指令級並行重排序:處理器採用了指令級並行技術來將多條指令重疊執行。如果不存在資料依賴性,處理器可以改變語句對應及其的執行順序。
  • 記憶體系統的重排序:處理器使用快取和讀/寫緩衝區,使得載入和儲存操作看上去可能是亂序執行。

舉個例來說明一下多執行緒中可能出現的重排現象:

class ReOrderDemo {
    int a = 0;
    boolean flag = false;
 
    public void write() {
        a = 1;                   //1
        flag = true;             //2
    }
     
    public void read() {
        if (flag) {                //3
            int i =  a * a;        //4
            ……
        }
    }
}

在上面的程式碼中,單執行緒執行時,read方法能夠獲得flag的值進行判斷,獲得預期結果。但在多執行緒的情況下就可能出現不同的結果。

比如,當執行緒A進行write操作時,由於指令重排,write方法中的程式碼執行順序可能會變成下面這樣:

flag = true;             //2
a = 1;                   //1

也就是說可能會先對flag賦值,然後再對a賦值。這在單執行緒中並不影響最終輸出的結果。

但如果與此同時,B執行緒在呼叫read方法,那麼就有可能出現flag為true但a還是0,這時進入第4步操作的結果就為0,而不是預期的1了。

請記住,指令重排只會保證單執行緒中序列語義執行的一致性,不會關心多執行緒間語義的一致性。這也是為什麼在寫單例模式時需要考慮新增volatile關鍵詞來修飾,就是為了防止指令重排導致的問題。

JMM提供的解決方案

在瞭解了原子性、可見性以及有序性問題後,看看JMM是提供了什麼機制來保證這些特性的。

原子性問題,除了JVM自身提供的對基本資料型別讀寫操作的原子性外,對於方法級別或者程式碼塊級別的原子性操作,可以使用synchronized關鍵字或者重入鎖(ReentrantLock)保證程式執行的原子性。

而工作記憶體與主記憶體同步延遲現象導致的可見性問題,可以使用synchronized關鍵字或者volatile關鍵字解決。它們都可以使一個執行緒修改後的變數立即對其他執行緒可見。

對於指令重排導致的可見性問題和有序性問題,則可以利用volatile關鍵字解決。volatile的另一個作用就是禁止重排序優化。

除了靠sychronized和volatile關鍵字之外,JMM內部還定義一套happens-before(先行發生)原則來保證多執行緒環境下兩個操作間的原子性、可見性以及有序性。

先行發生原則

如果僅靠sychronized和volatile關鍵字來保證原子性、可見性以及有序性,那麼編寫併發程式會十分麻煩。為此在Java記憶體模型中,還提供了happens-before原則來輔助保證程式執行的原子性、可見性以及有序性的問題。該原則是判斷資料是否存在競爭、執行緒是否安全的依據。

happens-before規則:

  • 程式次序規則:在一個執行緒內,程式前面的操作先於後面的操作。
  • 監視器鎖規則:一個unlock操作先於後面對同一個鎖的lock操作發生。
  • volatile變數規則:對一個volatile變數的寫操作先行發生於後面對這個變數的讀操作,也就是說讀取的值肯定是最新的。
  • 執行緒啟動規則:Thread物件的start()方法呼叫先行發生於此執行緒的每一個動作。
  • 執行緒加入規則:Thread物件的結束先行發生於join()方法返回。
  • 執行緒中斷規則:對執行緒interrupt()方法的呼叫先行發生於被中斷執行緒的程式碼檢測到中斷事件的發生,可以通過interrupted()方法檢測到是否有中斷髮生。
  • 物件終結規則:一個物件的初始化完成(建構函式執行結束)先行發生於它的finalize()方法的開始。
  • 傳遞性:如果操作A先行發生於操作B,操作B先行發生於操作C,那麼操作A先行發生於操作C。

還拿上面的具體程式碼來進行說明:

class ReOrderDemo {
    int a = 0;
    boolean flag = false;
 
    public void write() {
        a = 1;                   //1
        flag = true;             //2
    }
     
    public void read() {
        if (flag) {                //3
            int i =  a * a;        //4
            ……
        }
    }
}

執行緒A呼叫write()方法,執行緒B呼叫read()方法,執行緒A先(時間上的先後)於執行緒B啟動,那麼執行緒B讀取到a的值是多少呢?

現在依據8條原則來進行對照。

兩個方法分別由執行緒A和執行緒B呼叫,不在同一個執行緒中,因此程式次序原則不適用。

沒有write()方法和read()方法都沒有使用同步手段,監視器鎖規則不適用。

沒有使用volatile關鍵字,volatile變數原則不適用。

與執行緒啟動、終止、中斷、物件終結規則、傳遞性都沒有關係,不適用。

因此,執行緒A和執行緒B的啟動時間雖然有先後,但執行緒B執行結果卻是不確定,也是說上述程式碼沒有適合8條原則中的任意一條,所以執行緒B讀取的值自然也是不確定的,換句話說就是執行緒不安全的。

修復這個問題的方式很簡單,要麼給write()方法和read()方法新增同步手段,如synchronized。或者給變數flag新增volatile關鍵字,確保執行緒A修改的值對執行緒B總是可見。

小結

在這篇文章中介紹了Java記憶體模型中一些原則,及其衍生出來保證這些原則的方式和方法。也是為我們下面學習volatile這個面試官最愛問的關鍵字的做好鋪墊。歡迎關注微信公眾號“程式新視界”繼續學習。

原文連結:《Java記憶體模型相關原則詳解》

《面試官》系列文章:

  • 《JVM之記憶體結構詳解》
  • 《面試官,不要再問我“Java GC垃圾回收機制”了》
  • 《面試官,Java8 JVM記憶體結構變了,永久代到元空間》
  • 《面試官,不要再問我“Java 垃圾收集器”了》
  • 《Java虛擬機器類載入器及雙親委派機制》
  • 《Java記憶體模型(JMM)詳解》
  • 《Java記憶體模型相關原則詳解》


程式新視界:精彩和成長都不容錯過

相關推薦

Java記憶體模型相關原則

在《Java記憶體模型(JMM)詳解》一文中我們已經講到了Java記憶體模型的基本結構以及相關操作和規則。而Java記憶體模型又是圍繞著在併發過程中如何處理原子性、可見性以及有序性這三個特徵來構建的。本篇文章就帶大家瞭解一下相關概念、原則等內容。 原子性 原子性即一個操作或一系列是不可中斷的。即使是在多個執行

java物件建立(記憶體模型)過程

概述 java物件建立分為兩個過程:宣告物件引用和建立物件實體。類資訊、物件引用、物件實體均在記憶體的不同區域。 記憶體結構 每一個java應用程式均會唯一的對應一個jvm例項,而這個jvm例

不止面試02-JVM記憶體模型面試題

第一部分:面試題 本篇文章我們將嘗試回答以下問題: 描述一下jvm的記憶體結構 描述一下jvm的記憶體模型 談一下你對常量池的理解 什麼情況下會發生棧記憶體溢位?和記憶體溢位有什麼不同? String str = new String(“abc”)建立了多少個例項? 第二部分:深入原理 ok,開始。怎們還

【搞定Java併發程式設計】第7篇:Java記憶體模型

上一篇:ThreadLocal詳解:https://blog.csdn.net/pcwl1206/article/details/84859661 其實在Java虛擬機器的學習中,我們或多或少都已經接觸過了有關Java記憶體模型的相關概念(點選檢視),只不過在Java虛擬機器中講的不夠詳細,因此

Java併發程式設計:JMM (Java記憶體模型) 以及與volatile關鍵字

計算機系統的一致性 在現代計算機作業系統中,多工處理幾乎是一項必備的功能,因為嵌入了多核處理器,計算機系統真正做到了同一時間執行若干個任務,是名副其實的多核系統。在多核系統中,為了提升CPU與記憶體的互動效率,一般都設定了一層 “快取記憶體區” 作為記憶體與處理器之間的緩衝,使得CPU在運算的過程中直接從快

Java記憶體模型

前言 Java記憶體模型(Java Memory Model,簡稱JMM),即Java虛擬機器定義的一種用來遮蔽各種硬體和作業系統的記憶體訪問差異,以實現讓java程式在各種平臺下都能夠達到一致的記憶體訪問效果的記憶體模型。本篇文章大致涉及到五個要點:Java記憶體模型的基礎,主要介紹JMM抽象結構;Jav

Java記憶體模型(JMM)

在Java JVM系列文章中有朋友問為什麼要JVM,Java虛擬機器不是已經幫我們處理好了麼?同樣,學習Java記憶體模型也有同樣的問題,為什麼要學習Java記憶體模型。它們的答案是一致的:能夠讓我們更好的理解底層原理,寫出更高效的程式碼。 就Java記憶體模型而言,它是深入瞭解Java併發程式設計的先決條件

JAVA學習筆記(併發程式設計 - 陸)- J.U.C之AQS及其相關元件

文章目錄 J.U.C之AQS-介紹 關於AQS裡的state狀態: 關於自定義資源共享方式: 關於同步器設計: 如何使用: 具體實現的思路: 設計思想: 基於AQS的同步元件: AQS小結:

淺談快取一致性原則Java記憶體模型(JMM)

Java記憶體模型(JMM)是一個概念模型,底層是計算機的暫存器、快取記憶體、主記憶體和CPU等。 多處理器環境下,共享資料的互動硬體裝置之間的關係: JMM: 從以上兩張圖中,談一談以下幾個概念: 1.快取一致性協議(MESI): 由於每個處

Java記憶體模型與happens-before原則

Java記憶體模型 Java記憶體模型不同於Jvm記憶體模型,Java記憶體模型(JMM)規定了JVM必須遵循一組最小保證,這組保證規定了對變數的寫入操作在何時將於其他執行緒可見。 在Java虛擬機器規範中試圖定義一種Java記憶體模型(Java Memor

Java七大設計原則與運用

開心一笑 【婚禮上,氣氛正高著,主持人問新郎:”你會不會愛新娘一輩子?新郎興高采烈的喊:”會”。主持人:”你會不會在新娘容顏憔悴,疾病纏身的時候離開她? 猴急的新郎興高采烈的喊:”會”!】 課程介紹 Java七大設計原則在工作中是非常重要的

Java 記憶體模型中的happens-before 原則

1、happens-before 原則闡述了多執行緒之間的記憶體可見性。也就是說它是用來規定某個執行緒修改的變數在什麼時候對其它執行緒可見的。 happens-before 原則是判斷資料是否存在競爭、執行緒是否安全的主要依據,依靠這個原則,我們才能解決在併發環境下兩操作之間是否可能存在

處理器、程序、執行緒、並行、併發、記憶體模型相關概念、併發程式設計中的三個概念 、Java記憶體模型、剖析volatile關鍵字、用volatile關鍵字的場景

處理器:即中央處理器(CPU,Central Processing Unit),它是一塊超大規模的積體電路,是一臺計算機的運算核心(Core)和控制核心( Control Unit)。它的功能主要是解釋計算機指令以及處理計算機軟體中的資料。 程序:程序(Process)是計算機中的程式關

java.text.Format及相關

Format Format是一個用於格式化語言環境敏感的資訊(如日期、訊息和數字)的抽象基類,直接已知子類有DateFormat, MessageFormat, NumberFormat。 Format定義了程式設計介面,用於將語言環境敏感的物件格式化為String(使用

java中資料在記憶體中的儲存

  1. 有這樣一種說法,如今爭鋒於IT戰場的兩大勢力,MS一族偏重於底層實現,Java一族偏重於系統架構。說法根據無從考證,但從兩大勢力各自的社群力量和圖書市場已有佳作不難看出,此說法不虛,但掌握Java的底層實現對Java程式設計師來說是至關重要的,本文介紹了Java中的資料在記憶體中的儲存。   

java記憶體模型中的先行發生原則

先行發生原則 前言 由上一篇,我們知道併發問題的一個原因是有序性,而java中volatile和synchronized可以保證有序性; 但是在java中,並不是所有的操作都是由volatile和synchronized實現的,java中存在”先行發生

Java網路程式設計與NIO2:JAVA NIO 一步步構建I/O多路複用的請求模型

微信公眾號【黃小斜】作者是螞蟻金服 JAVA 工程師,專注於 JAVA 後端技術棧:SpringBoot、SSM全家桶、MySQL、分散式、中介軟體、微服務,同時也懂點投資理財,堅持學習和寫作,相信終身學習的力量!關注公眾號後回覆”架構師“即可領取 Java基礎、進階、專案和架構師等免費學習資料,更有資料

Java網路程式設計和NIO3:IO模型Java網路程式設計模型

微信公眾號【Java技術江湖】一位阿里 Java 工程師的技術小站。(關注公眾號後回覆”Java“即可領取 Java基礎、進階、專案和架構師等免費學習資料,更有資料庫、分散式、微服務等熱門技術學習視訊,內容豐富,兼顧原理和實踐,另外也將贈送作者原創的Java學習指南、Java程式設計師面試指南等乾貨資源)

跨域問題相關知識(原生js和jquery兩種方法實現jsonp跨域)

syn con 加載 developer 兩種方法 ray exe 編寫 分組 1、同源策略 同源策略(Same origin policy),它是由Netscape提出的一個著名的安全策略。同源策略是一種約定,它是瀏覽器最核心也最基本的安全功能,如果缺少了同源策略,則瀏覽

Java concurrent AQS 源碼

ted node thread range skip nothing 共享 mea 需要 一、引言   AQS(同步阻塞隊列)是concurrent包下鎖機制實現的基礎,相信大家在讀完本篇博客後會對AQS框架有一個較為清晰的認識   這篇博客主要針對AbstractQ