Java內存模型以及線程安全的可見性問題

Java內存模型 VS JVM運行時數據區

首先Java內存模型(JMM)和JVM運行時數據區並不是一個東西,許多介紹Java內存模型的文章描述的堆,方法區,Java虛擬機棧,本地方法棧,程序計數器這東西並不是Java內存模型的內容而是JVM運行時數據區的內容。
要理解二者的區別就要了解《Java虛擬機規範》和《Java語言規範》。我們知道Java虛擬機上並不知只有Java語言,像JRuby, ,Scala,Kotlin,Groovy等也都運行在Java虛擬機上,而這些語言想要在Java虛擬機上運行就要遵守《Java虛擬機規範》,而JVM運行時數據區就是《Java虛擬機規範》的內容。而《Java語言規範》就只是針對Java語言的規範,它對Java內存模型做了詳細的描述。

什麼是Java內存模型(JMM)?

要了解Java內存模型,首先要了解什麼是內存模型,之間在CPU緩存和內存屏障 中我們瞭解到緩存一致性問題以及處理器優化的指令重排序問題。爲了保證併發編程中可以滿足原子性、可見性及有序性。有一個重要的概念,那就是——內存模型。它解決了 CPU 多級緩存、處理器優化、指令重排等導致的內存訪問問題,保證了併發場景下的一致性、原子性和有序性。而Java內存模型就是解決由於多線程通過共享內存進行通信時,存在的本地內存數據不一致、編譯器會對代碼指令重排序、處理器會對代碼亂序執行等帶來的問題的一種規範。目的是保證併發編程場景中的原子性、可見性和有序性。
Java內存模型可以分爲線程棧(或者叫工作內存,它是每個線程所獨有的)和堆(或者叫主內存,與JVM運行時數據區的堆並不是一個概念,它是所線程共享的),其大致邏輯圖如下:


JMM中的具體內容

Shared Variables定義

可以在線程之間共享的內存稱爲共享內存或堆內存
所有實例字段,靜態字段和數組元素都存儲在共享內存,這些字段和數組就是共享變量
衝突:如果至少有一個訪問是寫操作,那麼對同一個變量的兩次訪問是衝突的

這些能被多個線程訪問的共享變量是內存模型規範的對象

線程間操作

線程間操作指一個線程執行的操作可被其他線程感知或被其他線程直接影響
Java內存模型只描述線程間操作,不描述線程內操作,線程內操作按照線程內語義執行
線程間操作有:

  • read操作(一般讀,即非volatile讀)

  • write操作(一般寫,即非volatile寫)

  • volatile read

  • volatile write

  • Lock,Unlock

  • 線程的第一個和最後一個操作

  • 外部操作

對同步規則的定義

  • 對volatile變量V的寫入,與所有其它線程後續對V的讀同步

  • 對於監視器m的解鎖與所有後續操作對於m的加鎖同步

  • 對於每個屬性寫入默認值(0,false, null)與每個線程對其進行的操作同步

  • 啓動線程的操作與線程中的第一個操作同步

  • 線程T2的最後操作與線程T1發現T2已經結束同步

  • 如果線程T1中斷了T2,那麼線程T1的中斷操作與其他所有線程發現T2被中斷了同步

happens-before先行發生原則

happens-before關係用於描述兩個有衝突的動作之間的順序,如果一個action happens before 另一個action,則第一個操作對第二個操作可見,JVM需要實現如下happens-before規則:

  • 某個線程中的每個動作都happens-before該線程中該動作後面的操作

  • 某個管程中的unlock動作happens-before同一個管程上後續的lock操作

  • 對某個volatile字段的寫操作happens-before每個後續對該volatile字段的讀操作

  • 在某個對象上調用start()方法happens-before被啓動線程的任意動作

  • 如果在線程t1中成功執行了t2.join(),則t2中的所有操作對t1可見

  • 如果某個動作a happens-before動作b,且b happens-before動作c,則a happens-before c

final在JMM中的處理

final在該對象的構造函數中設置對象的字段,當線程看到該對象時,將始終看到該對象的final字段的正確構造版本。如果在構造函數中設置字段後發生讀取,則會看到該final字段分配的值,否則它將看到默認值。讀取該對象的final成員變量之前,先要讀取共享對象。
通常被 static final修飾的字段, 不能被修改。然而System.in, System.out, System.err被static final修飾卻可以修改,遺留問題,必須通過set方法改變,我們將這些字段稱爲寫保護,以區別於普通final字段。


Word Tearing字節處理

有些處理器(尤其是早期的Alphas處理器)沒有提供寫單個字節的功能。在這樣的處理器上更新byte數組,若只是簡單的讀取整個內容,更新對應的字節,然後將整個內容再寫回內存,將是不合法的。這個問題有時候被稱爲“字分裂(word tearing)”,更新字節有難度的處理器,就需要尋求其他方式來解決。因此,編程人員需要注意,儘量不要對byte[]中的元素進行重新賦值,更不要在多線程中這樣做。


可見性問題

可見性:主要是指一個線程對共享變量的寫入可以被後續另一個線程讀取到,也就說一個線程對共享變量的操作對另一個線程是可見的。
而可見性問題就是指一個線程對共享變量進行了寫入而其他的線程卻無法讀取到該線程寫入的結果,根據以下工作內存的緩存的模型我們可以知道,造成可見性的問題主要有兩方面,一個是數據在寫入的時候只是寫入了緩存而沒有寫入主內存,一個是數據在讀取的時候只是從緩存中讀取到了數據而沒有從主內存讀取數據。

可見性問題的解決方法 — volatile關鍵字

volatile關鍵字可以保證一個線程對共享變量的修改,能夠及時的被其他線程看到。
根據JMM中的happen before 和同步原則:

  • 對某個volatile字段的寫操作happens-before每個後續對該volatile字段的讀操作

  • 對volatile變量V的寫入,與所有其它線程後續對V的讀同步


    而要滿足這些條件volatile關鍵字就具有以下功能:

  • 禁止緩存,volatile變量的訪問控制符會加個ACC_VOLATILE,《Java虛擬機規範》 中的對它的描述就是“cannot be cached”

  • 對volatile變量相關的指令不做重排序

本文分享自微信公衆號 - Coding Diary(gh_7e1f05090980)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章