一個volatile跟面試官扯了半個小時

《安琪拉與面試官二三事》系列文章,本文是此係列第三篇
一個HashMap能跟面試官扯上半個小時html

一個synchronized跟面試官扯了半個小時前端

歡迎關注Wx公衆號:【安琪拉的博客】—揭祕Java後端技術,還原技術背後的本質java

前言

volatile 應該算是Java 後端面試的必考題,由於多線程編程基本繞不開它,很適合做爲併發編程的入門題。git

開場

面試官:你先自我介紹一下吧!程序員

安琪拉: 我是安琪拉,草叢三婊之一,最強中單(鍾馗不服)!哦,不對,串場了,我是**,目前在–公司作–系統開發。github

面試官: 看你簡歷上寫熟悉併發編程,volatile 用過的吧?web

安琪拉: 用過的。(仍是熟悉的味道)面試

面試官: 那你跟我講講何時會用到 volatile ?算法

安琪拉: 若是須要保證多線程共享變量的可見性時,可使用volatile 來修飾變量。編程

面試官: 什麼是共享變量的可見性?

安琪拉: 多線程併發編程中主要圍繞着三個特性實現。可見性是其中一種!

  • 可見性

    可見性是指當多個線程訪問同一個共享變量時,一個線程修改了這個變量的值,其餘線程可以當即看到修改後的值。

  • 原子性

    原子性指的一個操做或一組操做要麼所有執行,要麼所有不執行。

  • 有序性

    有序性是指程序執行的順序按照代碼的前後順序執行。

面試官: volatile 除了解決共享變量的可見性,還有別的做用嗎?

安琪拉: volatile 除了讓共享變量具備可見性,還具備有序性(禁止指令重排序)。

面試官: 你先跟我舉幾個實際volatile 實際項目中的例子?

安琪拉: 能夠的。有個特別常見的例子:

  1. 狀態標誌

    好比咱們工程中常常用一個變量標識程序是否啓動、初始化完成、是否中止等,以下:

    volatile修飾狀態標誌
    volatile修飾狀態標誌

    volatile 很適合只有一個線程修改,其餘線程讀取的狀況。volatile 變量被修改以後,對其餘線程當即可見。

面試官: 如今咱們來看一下你的例子,若是不加volatile 修飾,會有什麼後果?

安琪拉: 好比這是一個帶前端交互的系統,有A、 B二個線程,用戶點了中止應用按鈕,A 線程調用shutdown() 方法,讓變量shutdown 從false 變成 true,可是由於沒有使用volatile 修飾, B 線程可能感知不到shutdown 的變化,而繼續執行 doWork 內的循環,這樣違背了程序的意願:當shutdown 變量爲true 時,表明應用該停下了,doWork函數應該跳出循環,再也不執行。

面試官: volatile還有別的應用場景嗎?

安琪拉: 懶漢式單例模式,咱們經常使用的 double-check 的單例模式,以下所示:

懶漢式單例模式
懶漢式單例模式

使用volatile 修飾保證 singleton 的實例化可以對全部線程當即可見。

面試官: 咱們再來看你的單例模式的例子,我有三個問題:

  1. 爲何使用volatile 修飾了singleton 引用還用synchronized 鎖?
  2. 第一次檢查singleton 爲空後爲何內部還須要進行第二次檢查?
  3. volatile 除了內存可見性,還有別的做用嗎?

安琪拉: 【內心炸了,舉單例模式例子簡直給本身挖坑】這三個問題,我來一個個回答:

  1. 爲何使用volatile 修飾了singleton 引用還用synchronized 鎖?

    volatile 只保證了共享變量 singleton 的可見性,可是 singleton = new Singleton(); 這個操做不是原子的,能夠分爲三步:

    步驟1:在堆內存申請一塊內存空間;

    步驟2:初始化申請好的內存空間;

    步驟3:將內存空間的地址賦值給 singleton;

    因此singleton = new Singleton(); 是一個由三步操做組成的複合操做,多線程環境下A 線程執行了第一步、第二步以後發生線程切換,B 線程開始執行第一步、第二步、第三步(由於A 線程singleton 是尚未賦值的),因此爲了保障這三個步驟不可中斷,可使用synchronized 在這段代碼塊上加鎖。(synchronized 原理參考《安琪拉與面試官二三事》系列第二篇文章)

  2. 第一次檢查singleton 爲空後爲何內部還進行第二次檢查?

    A 線程進行判空檢查以後開始執行synchronized代碼塊時發生線程切換(線程切換可能發生在任什麼時候候),B 線程也進行判空檢查,B線程檢查 singleton == null 結果爲true,也開始執行synchronized代碼塊,雖然synchronized 會讓二個線程串行執行,若是synchronized代碼塊內部不進行二次判空檢查,singleton 可能會初始化二次。

  3. volatile 除了內存可見性,還有別的做用嗎?

    volatile 修飾的變量除了可見性,還能防止指令重排序。

    指令重排序 是編譯器和處理器爲了優化程序執行的性能而對指令序列進行重排的一種手段。現象就是CPU 執行指令的順序可能和程序代碼的順序不一致,例如 a = 1; b = 2; 可能 CPU 先執行b=2; 後執行a=1;

    singleton = new Singleton(); 由三步操做組合而成,若是不使用volatile 修飾,可能發生指令重排序。步驟3 在步驟2 以前執行,singleton 引用的是尚未被初始化的內存空間,別的線程調用單例的方法就會引起未被初始化的錯誤。

    指令重排序也遵循必定的規則:

    • 重排序不會對存在依賴關係的操做進行重排

      指令重排
      指令重排
    • 重排序目的是優化性能,無論怎樣重排,單線程下的程序執行結果不會變

      as-if-serial
      as-if-serial

    所以volatile 還有禁止指令重排序的做用。

面試官: 那爲何不加volatile ,A 線程對共享變量的修改,其餘線程不可見呢?你知道volatile的底層原理嗎?

安琪拉: 果真該來的仍是來了,我要放大招了,您坐穩咯!

面試官: 我靠在椅子上,穩的很,請開始你的表演!

安琪拉: 先說結論,咱們知道volatile能夠實現內存的可見性和防止指令重排序,可是volatile 不保證操做的原子性。那麼volatile是怎麼實現可見性和有序性的呢?其實volatile的這些內存語意是經過內存屏障技術實現的。

面試官: 那你跟我講講內存屏障。

安琪拉: 講內存屏障的話,這塊內容會比較深,我如下面的順序講,這個整個知識成體系,不散:

  1. 現代CPU 架構的造成
  2. Java 內存模型(JMM)
  3. Java 經過 Java 內存模型(JMM )實現 volatile 平臺無關

現代CPU 架構的造成

安琪拉: 一切要從盤古開天闢地提及,女媧補天! 咳咳,很差意思,扯遠了! 一切從馮洛伊曼計算機體系開始提及!

面試官: 扯的是否是有點遠!

安琪拉: 你就說要不要聽?要聽別打斷我!

面試官: 得嘞!您請講!

安琪拉: 下圖就是經典的 馮洛伊曼體系結構,基本把計算機的組成模塊都定義好了,如今的計算機都是以這個體系弄的,其中最核心的就是由運算器和控制器組成的中央處理器,就是咱們常說的CPU。

image-20200509215308193
image-20200509215308193

面試官: 這個跟 volatile 有什麼關係?

安琪拉: 不要着急嘛!理解技術不要死盯着技術的細枝末節,要思考這個技術產生的歷史背景和緣由,思考發明這個技術的人當時是遇到了什麼問題? 而發明這個技術的。 這樣即理解深入,也讓本身思考問題更宏觀,更有深度!這叫從歷史的角度看問題,站在巨人的肩膀上!

面試官: 來來來,今天你教我作人!

安琪拉: 剛纔說到馮洛伊曼體系中的CPU,你應該聽過摩爾定律吧! 就是英特爾創始人戈登·摩爾講的:

集成電路上可容納的晶體管數目,約每隔18個月便會增長一倍,性能也將提高一倍。

面試官: 聽過的,而後呢?

安琪拉:因此你看到咱們電腦CPU 的性能愈來愈強勁,英特爾CPU 從Intel Core 一直到 Intel Core i7,前些年單核CPU 的晶體管數量確實符合摩爾定律,看下面這張圖。

image-20200427212746249
image-20200427212746249

橫軸爲新CPU發明的年份,縱軸爲可容納晶體管的對數。全部的點近似成一條直線,這意味着晶體管數目隨年份呈指數變化,大概每兩年翻一番。

面試官: 後來呢? 這和今天說的 volatile,以及內存屏障有什麼關係?

安琪拉:彆着急啊!後來摩爾定律愈來愈撐不住了,可是更新換代的程序對電腦性能的指望和要求還在不斷上漲,就出現了下面的劇情。

他爲其Pentium 4新一代芯片取消上市而道歉, 近幾年來,英特爾不斷地在增長其處理器的運行速度。當前最快的一款,其速度已達3.4GHz,雖然強化處理器的運行速度,也加強了芯片運做效能,但速度提高卻使得芯片的能源消耗量增長,並衍生出冷卻芯片的問題。

所以,英特爾摒棄將心力集中在提高運行速度的作法,在將來幾年,將其芯片轉爲以多模核心(multi-core)的方式設計等其餘方式,來提高芯片的表現。多模核心的設計法是將多模核心置入單一芯片中。如此一來,這些核心芯片即能以較緩慢的速度運轉,除了可減小運轉消耗的能量,也能減小運轉生成的熱量。此外,集衆核心芯片之力,可提供較單一核心芯片更大的處理能力。 —《經濟學人》

image-20200427213352064
image-20200427213352064

安琪拉:固然上面貝瑞特固然只是在開玩笑,眼看摩爾定律撐不住了,後來怎麼處理的呢?一顆CPU 不行,咱們多來幾顆嘛!這就是如今咱們常見的多核CPU,四核8G 聽着熟悉不熟悉?固然徹底依據馮洛伊曼體系設計的計算機也是有缺陷的!

面試官: 什麼缺陷? 說說看。

安琪拉: CPU 運算器的運算速度遠比內存讀寫速度快,因此CPU 大部分時間都在等數據從內存讀取,運算完數據寫回內存。

面試官: 那怎麼解決?

安琪拉: 由於CPU 運行速度實在太快,主存(就是內存)的數據讀取速度和CPU 運算速度差了有幾個數量級,所以現代計算機系統經過在CPU 和主存以前加了一層讀寫速度儘量接近CPU 運行速度的高速緩存來作數據緩衝,這樣緩存提早從主存獲取數據,CPU 再也不從主存取數據,而是從緩存取數據。這樣就緩解因爲主存速度太慢致使的CPU 飢餓的問題。同時CPU 內還有寄存器,一些計算的中間結果臨時放在寄存器內。

面試官: 既然你提到緩存,那我問你一個問題,CPU 從緩存讀取數據和從內存讀取數據除了讀取速度的差別?有什麼本質的區別嗎?不都是讀數據寫數據,並且加緩存會讓整個體系結構變得更加複雜。

安琪拉:緩存和主存不只僅是讀取寫入數據速度上的差別,還有另外更大的區別:研究人員發現了程序80%的時間在運行20% 的代碼,因此緩存本質上只要把20%的經常使用數據和指令放進來就能夠了(是否是和Redis 存放熱點數據很像),另外CPU 訪問主存數據時存在二個局部性現象:

  1. 時間局部性現象

    若是一個主存數據正在被訪問,那麼在近期它被再次訪問的機率很是大。想一想你程序大部分時間是否是在運行主流程。

  2. 空間局部性現象

    CPU使用到某塊內存區域數據,這塊內存區域後面臨近的數據很大機率當即會被使用到。這個很好解釋,咱們程序常常用的數組、集合(本質也是數組)常常會順序訪問(內存地址連續或鄰近)。

由於這二個局部性現象的存在使得緩存的存在能夠很大程度上緩解CPU 飢餓的問題。

面試官: 講的是那麼回事,那能給我畫一下如今CPU、緩存、主存的關係圖嗎?

安琪拉:能夠。咱們來看下如今主流的多核CPU的硬件架構,以下圖所示。

多核心CPU架構
多核心CPU架構

安琪拉: 現代操做系統通常會有多級緩存(Cache Line),通常有L一、L2,甚至有L3,看下安琪拉的電腦緩存信息,一共4核,三級緩存,L1 緩存(在CPU核心內)這裏沒有顯示出來,這裏L2 緩存後面括號標識了是每一個核都有L2 緩存,而L3 緩存沒有標識,是由於L3 緩存是4個核共享的緩存:

安琪拉的電腦緩存
安琪拉的電腦緩存

面試官: 那你能跟我簡單講講程序運行時,數據是怎麼在主存、緩存、CPU寄存器之間流轉的嗎?

安琪拉: 能夠。好比以 i = i + 2; 爲例, 當線程執行到這條語句時,會先從主存中讀取i 的值,而後複製一份到緩存中,CPU 讀取緩存數據(取數指令),進行 i + 2 操做(中間數據放寄存器),而後把結果寫入緩存,最後將緩存中i最新的值刷新到主存當中(寫回時間不肯定)。

面試官: 這個數據操做邏輯在單線程環境和多線程環境下有什麼區別?

安琪拉: 好比i 若是是共享變量(例如對象的成員變量),單線程運行沒有任何問題,可是多線程中運行就有可能出問題。例如:有A、B二個線程,在不一樣的CPU 上運行,由於每一個線程運行的CPU 都有本身的緩存,A 線程從內存讀取i 的值存入緩存,B 線程此時也讀取i 的值存入本身的緩存,A 線程對i 進行+1操做,i變成了1,B線程緩存中的變量 i 仍是0,B線程也對i 進行+1操做,最後A、B線程前後將緩存數據寫入內存,內存預期正確的結果應該是2,可是實際是1。這個就是很是著名的緩存一致性問題

說明:單核CPU 的多線程也會出現上面的線程不安全的問題,只是產生緣由不是多核CPU緩存不一致的問題致使,而是CPU調度線程切換,多線程局部變量不一樣步引發的。

執行過程以下圖:

緩存不一致
緩存不一致

面試官: 那CPU 怎麼解決緩存一致性問題呢?

安琪拉:早期的一些CPU 設計中,是經過鎖總線(總線訪問加Lock# 鎖)的方式解決的。看下CPU 體系結構圖,以下:

CPU內體系結構
CPU內體系結構

由於CPU 都是經過總線來讀取主存中的數據,所以對總線加Lock# 鎖的話,其餘CPU 訪問主存就被阻塞了,這樣防止了對共享變量的競爭。可是鎖總線對CPU的性能損耗很是大,把多核CPU 並行的優點直接給乾沒了!

後面研究人員就搞出了一套協議:緩存一致性協議。協議的類型不少(MSI、MESI、MOSI、Synapse、Firefly),最多見的就是Intel 的MESI 協議。緩存一致性協議主要規範了CPU 讀寫主存、管理緩存數據的一系列規範,以下圖所示。

緩存一致性協議
緩存一致性協議

面試官: 那講講 **MESI **協議唄!

安琪拉: (MESI這部份內容能夠只瞭解大概思想,不用深究,由於東西多到能夠單獨成一篇文章了)

MESI 協議的核心思想:

  • 定義了緩存中的數據狀態只有四種,MESI 是四種狀態的首字母。
  • 當CPU寫數據時,若是寫的變量是共享變量,即在其餘CPU中也存在該變量的副本,會發出信號通知其餘CPU將該變量的緩存行置爲無效狀態;
  • 當CPU讀取共享變量時,發現本身緩存的該變量的緩存行是無效的,那麼它就會從內存中從新讀取。

緩存中數據都是以緩存行(Cache Line)爲單位存儲;MESI 各個狀態描述以下表所示:

image-20200512091902093
image-20200512091902093

面試官: 那我問你MESI 協議和volatile實現的內存可見性時什麼關係?

安琪拉: volatile 和MESI 中間差了好幾層抽象,中間會經歷java編譯器,java虛擬機和JIT,操做系統,CPU核心。

volatile 是Java 中標識變量可見性的關鍵字,說直接點:使用volatile 修飾的變量是有內存可見性的,這是Java 語法定的,Java 不關心你底層操做系統、硬件CPU 是如何實現內存可見的,個人語法規定就是volatile 修飾的變量必須是具備可見性的。

CPU 有X86(複雜指令集)、ARM(精簡指令集)等體系架構,版本類型也有不少種,CPU 可能經過鎖總線、MESI 協議實現多核心緩存的一致性。由於有硬件的差別以及編譯器和處理器的指令重排優化的存在,因此Java 須要一種協議來規避硬件平臺的差別,保障同一段表明在全部平臺運行效果一致,這個協議叫作Java 內存模型(Java Memory Model)。

Java 內存模型(JMM)

面試官: 你能詳細講講Java 內存模型嗎?

安琪拉: JMM 全稱 Java Memory Model, 是 Java 中很是重要的一個概念,是Java 併發編程的核心和基礎。JMM 是Java 定義的一套協議,用來屏蔽各類硬件和操做系統的內存訪問差別,讓Java 程序在各類平臺都能有一致的運行效果。

協議這個詞都不會陌生,HTTP 協議、TCP 協議等。JMM 協議就是一套規範,具體的內容爲:

全部的變量都存儲在主內存中,每一個線程還有本身的工做內存,線程的工做內存中保存了該線程使用到的變量(主內存的拷貝),線程對變量的全部操做(讀取、賦值)都必須在工做內存中進行,而不能直接讀寫主內存中的變量。不一樣線程之間沒法直接訪問對方工做內存中的變量,線程間變量值的傳遞均須要在主內存來完成。

面試官: 你剛纔提到每一個線程都有本身的工做內存,問個深刻一點的問題,線程的工做內存在主存仍是緩存中

安琪拉: 這個問題很是棒!JMM 中定義的每一個線程私有的工做內存是抽象的規範,實際上工做內存和真實的CPU 內存架構以下所示,Java 內存模型和真實硬件內存架構是不一樣的:

JMM與真實內存架構
JMM與真實內存架構

JMM 是內存模型,是抽象的協議。首先真實的內存架構是沒有區分堆和棧的,這個Java 的JVM 來作的劃分,另外線程私有的本地內存線程棧可能包括CPU 寄存器、緩存和主存。堆亦是如此!

面試官: 能具體講講JMM 內存模型規範嗎?

安琪拉: 能夠。前面已經講了線程本地內存和物理真實內存之間的關係,說的詳細些:

  • 初始變量首先存儲在主內存中;
  • 線程操做變量須要從主內存拷貝到線程本地內存中;
  • 線程的本地工做內存是一個抽象概念,包括了緩存、store buffer(後面會講到)、寄存器等。
JMM
JMM

面試官: 那JMM 模型中多線程如何經過共享變量通訊呢?

安琪拉: 線程間通訊必需要通過主內存。

線程A與線程B之間要通訊的話,必需要經歷下面2個步驟:

1)線程A把本地內存A中更新過的共享變量刷新到主內存中去。

2)線程B到主內存中去讀取線程A以前已更新過的共享變量。

線程間通訊
線程間通訊

關於主內存與工做內存之間的具體交互協議,即一個變量如何從主內存拷貝到工做內存、如何從工做內存同步到主內存之間的實現細節,Java內存模型定義瞭如下八種操做(單一操做都是原子的)來完成:

  • lock(鎖定):做用於主內存的變量,把一個變量標識爲一條線程獨佔狀態。
  • unlock(解鎖):做用於主內存變量,把一個處於鎖定狀態的變量解除鎖定,解除鎖定後的變量才能夠被其餘線程鎖定。
  • read(讀取):做用於主內存變量,把一個變量值從主內存傳輸到線程的工做內存中,以便隨後的load動做使用
  • load(載入):做用於工做內存的變量,它把read操做從主內存中獲得的變量值放入工做內存的變量副本中。
  • use(使用):做用於工做內存的變量,把工做內存中的一個變量值傳遞給執行引擎,每當虛擬機遇到一個須要使用變量的值的字節碼指令時將會執行這個操做。
  • assign(賦值):做用於工做內存的變量,它把一個從執行引擎接收到的值賦值給工做內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操做。
  • store(有的指令是save/存儲):做用於工做內存的變量,把工做內存中的一個變量的值傳送到主內存中,以便隨後的write的操做。
  • write(寫入):做用於主內存的變量,它把store操做從工做內存中一個變量的值傳送到主內存的變量中。

咱們編譯一段Java code 看一下。

代碼和字節碼指令分別爲:

指令演示源代碼
指令演示源代碼
指令演示
指令演示
image-20200511151603104
image-20200511151603104

Java內存模型還規定了在執行上述八種基本操做時,必須知足以下規則:

  • 若是要把一個變量從主內存中複製到工做內存,須要順序執行read 和load 操做, 若是把變量從工做內存中同步回主內存中,就要按順序地執行store 和write 操做。但Java內存模型只要求上述操做必須按順序執行,而沒有保證必須是連續執行,也就是操做不是原子的,一組操做能夠中斷。
  • 不容許read和load、store和write操做之一單獨出現,必須成對出現。
  • 不容許一個線程丟棄它的最近assign的操做,即變量在工做內存中改變了以後必須同步到主內存中。
  • 不容許一個線程無緣由地(沒有發生過任何assign操做)把數據從工做內存同步回主內存中。
  • 一個新的變量只能在主內存中誕生,不容許在工做內存中直接使用一個未被初始化(load或assign)的變量。即就是對一個變量實施use和store操做以前,必須先執行過了assign和load操做。
  • 一個變量在同一時刻只容許一條線程對其進行lock操做,但lock操做能夠被同一條線程重複執行屢次,屢次執行lock後,只有執行相同次數的unlock操做,變量纔會被解鎖。lock和unlock必須成對出現
  • 若是對一個變量執行lock操做,將會清空工做內存中此變量的值,在執行引擎使用這個變量前須要從新執行load或assign操做初始化變量的值
  • 若是一個變量事先沒有被lock操做鎖定,則不容許對它執行unlock操做;也不容許去unlock一個被其餘線程鎖定的變量。
  • 對一個變量執行unlock操做以前,必須先把此變量同步到主內存中(執行store和write操做)。

面試官: 聽下來 Java 內存模型真的內容不少,那Java 內存模型是如何保障你上面說的這些規則的呢?

安琪拉: 這就是接下來要說的底層實現原理了,上面叨逼叨說了一堆概念和規範,須要慢慢消化。

Java 經過 Java 內存模型(JMM )實現 volatile 平臺無關

安琪拉: 咱們前面說 併發編程實際就是圍繞三個特性的實現展開的:

  • 可見性
  • 有序性
  • 原子性

面試官: 對的。前面已經說過了。我怎麼感受我想是捧哏。 😁

安琪拉: 前面咱們已經說過共享變量不可見的問題,講完Java 內存模型,理解的應該更深入了,以下圖所示:

image-20200511155804771
image-20200511155804771

1. 可見性問題:若是對象obj 沒有使用volatile 修飾,A 線程在將對象count讀取到本地內存,從1修改成2,B 線程也把obj 讀取到本地內存,由於A 線程的修改對B 線程不可見,這是從Java 內存模型層面看可見性問題(前面從物理內存結構分析的)。

2. 有序性問題:重排序發生的地方有不少,編譯器優化、CPU 由於指令流水批處理而重排序、內存由於緩存以及store buffer 而顯得亂序執行。以下圖所示:

image-20200511163223157
image-20200511163223157

附一張帶store buffer (寫緩衝)的CPU 架構圖,但願詳細瞭解store buffer 能夠看文章最後面的擴展閱讀。

image-20200511163359152
image-20200511163359152

每一個處理器上的Store Buffer(寫緩衝區),僅僅對它所在的處理器可見。這會致使處理器執行內存操做的順序可能會與內存實際的操做執行順序不一致。因爲現代的處理器都會使用寫緩衝區,所以現代的處理器都會容許對寫-讀操做進行重排序:

下圖是各類CPU 架構容許的指令重排序的狀況。

image-20200511165457535
image-20200511165457535

3. 原子性問題:例如多線程併發執行 i = i +1。 i 是共享變量,看完Java 內存模型,知道這個操做不是原子的,能夠分爲+1 操做和賦值操做。所以多線程併發訪問時,可能發生線程切換,形成不是預期結果。

針對上面的三個問題,Java 中提供了一些關鍵字來解決。

  1. 可見性 & 有序性 問題解決

    volatile 可讓共享變量實現可見性,同時禁止共享變量的指令重排,保障可見性。從JSR-333 規範 和 實現原理講:

    • JSR-333 規範:JDK 5定義的內存模型規範,

      在JMM中,若是一個操做執行的結果須要對另外一個操做可見,那麼這兩個操做之間必須存在happens-before關係。

      1. 若是一個操做happens-before另外一個操做,那麼第一個操做的執行結果將對第二個操做可見,並且第一個操做的執行順序排在第二個操做以前。

      2. 兩個操做之間存在happens-before關係,並不意味着必定要按照happens-before原則制定的順序來執行。若是重排序以後的執行結果與按照happens-before關係來執行的結果一致,那麼這種重排序並不非法。

      1. 程序次序規則:一個線程內,按照代碼順序,書寫在前面的操做先行發生於書寫在後面的操做;
      2. 鎖定規則:一個unLock操做先行發生於後面對同一個鎖額lock操做;
      3. volatile變量規則:對一個變量的寫操做先行發生於後面對這個變量的讀操做;
      4. 傳遞規則:若是操做A先行發生於操做B,而操做B又先行發生於操做C,則能夠得出操做A先行發生於操做C;
      5. 線程啓動規則:Thread對象的start()方法先行發生於此線程的每一個一個動做;
      6. 線程中斷規則:對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生;
      7. 線程終結規則:線程中全部的操做都先行發生於線程的終止檢測,咱們能夠經過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執行;
      8. 對象終結規則:一個對象的初始化完成先行發生於他的finalize()方法的開始;
    • 實現原理:上面說的happens-before原則保障可見性,禁止指令重排保證有序性,如何實現的呢?

      Java編譯器在生成指令序列的適當位置會插入內存屏障指令來禁止特定類型的處理器重排序,保證共享變量操做的有序性。

      內存屏障指令:寫操做的會讓線程本地的共享內存變量寫完強制刷新到主存。讀操做讓本地線程變量無效,強制從主內存讀取,保證了共享內存變量的可見性。

    JVM中提供了四類內存屏障指令:

    image-20200512091721797
    image-20200512091721797

    JSR-133 定義的相應的內存屏障,在第一步操做(列)和第二步操做(行)之間須要的內存屏障指令以下:

    image-20200511174714486
    image-20200511174714486

    Java volatile 例子:

    image-20200511175002261
    image-20200511175002261

    如下是區分各個CPU體系支持的內存屏障(也叫內存柵欄),由JVM 實現平臺無關(volatile全部平臺表現一致)

    image-20200511172853931
    image-20200511172853931

    synchronized 也能夠實現有序性和可見性,可是是經過鎖讓併發串行化實現有序,內存屏障實現可見。原理能夠看《安琪拉與面試官二三事》系列的synchronized 篇。

    • 一個線程寫入變量a後,任何線程訪問該變量都會拿到最新值。
    • 在寫入變量a以前的寫入操做,其更新的數據對於其餘線程也是可見的。由於Memory Barrier會刷出cache中的全部先前的寫入。
  2. 原子性問題解決

    原子性主要經過JUC Atomic***包實現,以下圖所示,內部使用CAS 指令實現原子性,各個CPU架構有些區別。

擴展閱讀

Java如何實現跨平臺

做爲Java 程序員的咱們只須要寫一堆 ***.java 文件,編譯器把 .java 文件編譯成 .class 字節碼文件,後面的事就都交給Java 虛擬機(JVM)作了。以下圖所示, Java虛擬機是區分平臺的,虛擬機來進行 .class 字節碼指令翻譯成平臺相關的機器碼。

image-20200509180128896
image-20200509180128896

因此 Java 是跨平臺的,Java 虛擬機(JVM)不是跨平臺的,JVM 是平臺相關的。 你們能夠看 Hostpot1.8 源碼文件夾,JVM 每一個系統都有單獨的實現,以下圖所示:

image-20200509181352578
image-20200509181352578
As-if-serial

As-if-serial語義的意思是,全部的動做(Action)均可覺得了優化而被重排序,可是必須保證它們重排序後的結果和程序代碼自己的應有結果是一致的。Java編譯器、運行時和處理器都會保證單線程下的as-if-serial語義。

併發&並行

現代操做系統,現代操做系統都是按時間片調度執行的,最小的調度執行單元是線程,多任務和並行處理能力是衡量一臺計算機處理器的很是重要的指標。這裏有個概念要說一下:

  • 併發:多個程序可能同時運行的現象,例如刷微博和聽歌同時進行,可能你電腦只有一顆CPU,可是經過時間片輪轉的方式讓你感受在同時進行。
  • 並行:多核CPU,每一個CPU 內運行本身的線程,是真正的同時進行的,叫並行。
內存屏障

JSR-133 對應規則須要的規則

image-20200511174714486
image-20200511174714486

另外 final 關鍵字須要 StoreStore 屏障

x.finalField = v; StoreStore; sharedRef = x;

MESI 協議運做模式

MESI 協議運做的具體流程,舉個實例

image-20200511161720436
image-20200511161720436

第一列是操做序列號,第二列是執行操做的CPU,第三列是具體執行哪種操做,第四列描述了各個cpu local cache中的cacheline的狀態(用meory address/狀態表示),最後一列描述了內存在0地址和8地址的數據內容的狀態:V表示是最新的,和cache一致,I表示不是最新的內容,最新的內容保存在cache中。

總結篇

Java內存模型

Java 內存模型(JSR-133)屏蔽了硬件、操做系統的差別,實現讓Java程序在各類平臺下都能達到一致的併發效果,規定了一個線程如何和什麼時候能夠看到由其餘線程修改事後的共享變量的值,以及在必須時如何同步的訪問共享變量,JMM使用內存屏障提供了java程序運行時統一的內存模型。

volatile的實現原理

volatile能夠實現內存的可見性和防止指令重排序。

經過內存屏障技術實現的。

爲了實現volatile的內存語義,編譯器在生成字節碼時,會在指令序列中插入內存屏障指令,內存屏障效果有:

  • 禁止volatile 修飾變量指令的重排序

  • 寫入數據強制刷新到主存

  • 讀取數據強制從主存讀取

volatile使用總結
  • volatile 是Java 提供的一種輕量級同步機制,能夠保證共享變量的可見性和有序性(禁止指令重排),經常使用於

    狀態標誌、雙重檢查的單例等場景。使用原則:

    • 對變量的寫操做不依賴於當前值。例如 i++ 這種就不適用。
    • 該變量沒有包含在具備其餘變量的不變式中。

    volatile的使用場景不是不少,使用時須要仔細考慮下是否適用volatile,注意知足上面的二個原則。

  • 單個的共享變量的讀/寫(好比a=1)具備原子性,可是像num++或者a=b+1;這種複合操做,volatile沒法保證其原子性;

題外話

  1. 你們知道我喜歡經過對話、面試問答的形式寫文章,並不必定是面試會問的問題,只是帶着問題看安琪拉的回答印象會更加深入!你們在看文章的時候能夠先不着急看回答,思考遇到這個問題本身會怎麼回答,而後內心想的答案和安琪拉的回答對比,這樣邊思考邊閱讀,收貨更多!
  2. 我文章寫得比較慢,主要是喜歡一篇文章把全部事情講清楚講透,你們在看的時候不要急着一次所有弄懂,看到不理解的地方能夠留言,或者先收藏,沒事多看幾遍。
  3. 另外文章都比較長,但願由淺入深,按部就班把使用和原理講清楚,不管是初學者仍是資深開發看完個人文章都能有收穫。

關注Wx公衆號:【安琪拉的博客】** —揭祕Java後端技術,還原技術背後的本質

文章列表

《安琪拉與面試官二三事》系列文章,本文是此係列第三篇 一個HashMap能跟面試官扯上半個小時

一個synchronized跟面試官扯了半個小時

《安琪拉教魯班學算法》系列文章

安琪拉教魯班學算法之動態規劃

安琪拉教魯班學算法之BFS和DFS

安琪拉教魯班學算法之堆排序

《安琪拉教妲己學分佈式》系列文章

安琪拉教妲己分佈式限流

《安琪拉教百里守約學併發編程》系列文章

安琪拉教百里守約學併發編程之多線程基礎

關注微信公衆號:【安琪拉的博客】—揭祕Java後端技術,還原技術背後的本質

參考文獻

《Java併發編程的藝術》 《深刻理解Java內存模型》 《深刻理解Java虛擬機》

JSR 133 (Java Memory Model) FAQ Why Memory Barriers?中文翻譯(上) Java Memory Model The JSR-133 Cookbook for Compiler Writers

相關文章
相關標籤/搜索