「原創」Java並發編程系列03|重排序-可見性和有序性問題根源

2020-12-15 酷扯兒

本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫

思維導圖

寫在前面

並發編程的三大問題:原子性、可見性、有序性。

緩存不能及時刷新導致了可見性問題。

編譯器為了優化性能而改變程序中語句的先後順序,導致有序性問題。

而「緩存不能及時刷新「和「編譯器為了優化性能而改變程序中語句的先後順序」都是重排序的一種。

那麼重排序到底是什麼?這篇文章就來介紹一下。

1. 重排序概念

在執行程序時為了提高性能,編譯器和處理器常常會對指令做重排序。

從java原始碼到最終實際執行的指令序列,會分別經歷下面三種重排序:

編譯器優化的重排序。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執行順序。指令級並行的重排序。處理器將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序。內存系統的重排序。處理器使用緩存和讀/寫緩衝區,使得加載和存儲操作看上去可能是在亂序執行。舉例:如下代碼執行過程中,程序不一定按照先A後B的順序執行,經重排序之後可能按照先B後A的順序執行。

int a = 1;// Aint b = 2;// B

2. 重排序規則

重排序需要遵守一定規則,以保證程序正確執行。

重排序遵守數據依賴性

數據依賴性:如果兩個操作訪問同一個變量,且這兩個操作中有一個為寫操作,此時這兩個操作之間就存在數據依賴性。

存在數據依賴性的三種情況:

① 寫後讀:a = 1;b = a; 寫一個變量之後,再讀這個位置。② 寫後寫:a = 1;a = 2; 寫一個變量之後,再寫這個變量。③ 讀後寫:a = b;b = 1;讀一個變量之後,再寫這個變量。

存在數據依賴關係的兩個操作,不可以重排序。

數據依賴性只針對單個處理器中執行的指令序列和單個線程中執行的操作。

舉例:

同一個線程中執行a=1;b=1; 不存在數據依賴性,可能重排序。同一個線程中執行a=1;b=a; 存在數據依賴性,不可以重排序。

重排序遵守as-if-serial 語義

as-if-serial 語義:不管怎麼重排序(編譯器和處理器為了提高並行度),(單線程)程序的執行結果不能被改變。

舉例,以計算圓的面積為例:

double pi = 3.14; // Adouble r = 1.0; // Bdouble area = pi * r * r; // C

A和B重排序之後,程序的執行結果不會改變,所以允許A、B重排序。A和C重排序之後,程序的執行結果會改變,所以不允許A、C重排序。

筆者看來,遵守數據依賴性和as-if-serial 語義實質上是一回事。為了遵守 as-if-serial 語義,編譯器和處理器不會對存在數據依賴關係的操作做重排序,因為這種重排序會改變執行結果。

3. 重排序帶來的問題

重排序可以提高程序執行的性能,但是代碼的執行順序改變,可能會導致多線程程序出現可見性問題和有序性問題

舉例:

初始狀態:a = b = 0;x = y = 0;Processor A:a = 1; // A1 x = b; // A2Processor B: b = 2; // B1 y = a; // B2

如上代碼,Processor A和Processor B同時執行,最終卻可能得到x = y = 0的結果。

原因分析:

第一步執行A1/B1將a=1寫到緩衝區,此時寫緩衝區還在等待其他寫操作,不執行A3,所以內存中的a=0;

第二步執行A2/B2,處理器讀取內存中的a,得到a=0;

雖然處理器 A 執行內存操作的順序為:A1->A2,但內存操作實際發生的順序卻是:A2->A1。此時,處理器 A 的內存操作順序被重排序了

4. JMM如何解決重排序問題

JMM處理重排序問題:

1)對於編譯器,JMM的編譯器重排序規則會禁止特定類型的編譯器重排序(不是所有的編譯器重排序都要禁止)。

2)對於處理器重排序,JMM的處理器重排序規則會要求Java編譯器在生成指令序列時,插入特定類型的內存屏障指令,來禁止特定類型的處理器重排序。

3)JMM根據代碼中的關鍵字(如:synchronized、volatile)和J.U.C包下的一些具體類來插入內存屏障。

JMM把內存屏障指令分為下列四類:

Store:數據對其他處理器可見(即:刷新到內存中)Load:讓緩存中的數據失效,重新從主內存加載數據

總結

在執行程序時為了提高性能,編譯器和處理器常常會對指令做重排序。

從Java原始碼到最終實際執行,要經歷三種重排序:編譯器優化的重排序、指令級並行的重排序、內存系統的重排序。

as-if-serial語義要求:不管怎麼重排序,程序的執行結果不能被改變。

存在數據依賴關係的兩個操作,不可以重排序。

重排序可能會導致多線程程序出現可見性問題和有序性問題。

JMM編譯時在當位置會插入內存屏障指令來禁止特定類型的重排序。

相關焦點

  • 「原創」Java並發編程系列02|並發編程三大核心問題
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫寫在前面編寫並發程序是比較困難的,因為並發程序極易出現Bug,這些Bug有都是比較詭異的,很多都是沒辦法追蹤,而且難以復現。
  • 原創】Java並發編程系列01|開篇獲獎感言
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫為什麼要學並發編程我曾聽一個從事15年開發工作的技術人員說過這個不寫並發程序的原則行得通的背景是那個時候基本都是單核處理器,系統並發量很低,藉助資料庫和類似Tomcat這種中間件就可以解決並發問題。如今硬體的驅動和網際網路行業的飛速發展,64核的處理器已經是很常見了,大型互聯廠商的系統並發量輕鬆過百萬,傳統的中間件和資料庫肯定是不能幫我們遮風避雨了,我們只能通過並發編程來解決這些問題。
  • Java並發編程之驗證volatile的可見性
    Java並發編程之驗證volatile的可見性通過系列文章的學習,凱哥已經介紹了volatile的三大特性。1:保證可見性 2:不保證原子性 3:保證順序。那麼怎麼來驗證可見性呢?本文凱哥將通過代碼演示來證明volatile的可見性。
  • Java並發編程學習前期知識上篇
    Java並發編程-前期準備知識-上我們先來看看幾個大廠真實的面試題:從上面幾個真實的面試問題來看,我們可以看到大廠的面試都會問到並發相關的問題。所以Java並發,這個無論是面試還是在工作中,並發都是會遇到的。
  • 深入分析volatile是如何實現可見性和有序性的
    並發編程中常常繞不開原子性、可見性與有序性的討論。先來看看什麼是原子性?什麼是原子性原子(atomic)本意是不能被進一步分割的最小粒子,而原子操作意為不可被中斷的一個或者一系列操作。volatile是不能保證原子性,但是在特定場景就是在32位處理器裡,對double和long型的變量的讀寫操作加了volatile修飾可以保證原子性。
  • Java的synchronized 能防止指令重排序嗎?
    「面試官」:二胖是吧,先做個自我介紹吧。「二胖」:好的,我叫二胖,我來自長沙,今年25歲,從事java開發快3年了,現在在XX公司XX事業部擔任高級「java」開發工程師,主要負責XX系統。。。。。「面試官」:好的,我看你簡歷上寫著熟練掌握並發編程你能跟我說說並發編程裡面你都知道哪些關鍵字。
  • 你應該要理解的java並發關鍵字volatile
    提高java的並發編程,就不得不提volatile關鍵字,不管是在面試還是實際開發中 volatile都是一個應該掌握的技能。他的重要性不言而喻。因此也有必要學好。一、為什麼要用到volatile關鍵字?
  • Java面試熱點學習:深入並發編程中的synchronized(前三章)
    第一章:並發編程中的三個問題可見性學習什麼是可見性問題可見性概念可見性(Visibility):是指一個線程對共享變量進行修改,另一個先立即得到修改後的最新值。小結並發編程時,會出現可見性問題,當一個線程對共享變量進行了修改,另外的線程並沒有立即看到修改後的最新值。
  • 「原創」Java並發編程系列09|基礎乾貨
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫本文為何適原創並發編程系列第9篇。現在,我們進入正題:介紹並發編程的基礎性概念。
  • 我和面試官的博弈:Java 並發編程篇
    日後我會把此系列整理成 PDF 版本,發布給大家。還請大家置頂(標星)本公眾號:Java後端,第一時間接收優質博面試中問的頻率很高的一個是分布式,一個就是並發。而JUC(java.util.concurrent)裡的東西是並發編程的基石。上次的面試已經過去一段時間,在一邊努力工作的同時,我也一邊抽出時間準備 Java 並發編程的部分。今天懷著輕鬆愉快的心情,再次踏上我的大廠面試之旅。
  • 1分鐘讀懂java中的volatile關鍵字
    2.volatile特性volatile具有可見性、有序性,不具備原子性。注意,volatile不具備原子性,這是volatile與java中的synchronized、java.util.concurrent.locks.Lock最大的功能差異,這一點在面試中也是非常容易問到的點。
  • Java程式設計師面試必備:Volatile全方位解析
    為了更好的執行性能,java內存模型並沒有限制執行引擎使用處理器的特定寄存器或緩存來和主內存打交道,也沒有限制編譯器進行調整代碼順序優化。所以Java內存模型 「會存在緩存一致性問題和指令重排序問題的」。 Java內存模型規定所有的變量都是存在主內存當中(類似於計算機模型中的物理內存),每個線程都有自己的工作內存(類似於計算機模型的高速緩存)。
  • 「原創」Java並發編程系列26|ConcurrentHashMap(上)
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫終於輪到ConcurrentHashMap了,並發編程必備,也是面試必備。HashMap 作為使用最頻繁的集合之一,在多線程環境下是不能用的,因為 HashMap 的設計上就沒有考慮並發環境,極易導致線程安全問題。為了解決該問題,提供了 Hashtable 和 Collections.synchronizedMap(hashMap)兩種解決方案,但是這兩種方案都是對讀寫加獨佔鎖,一個線程在讀時其他線程必須等待,吞吐量和性能都較低。
  • 「原創」Java並發編程系列28|Copy-On-Write容器
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫正文前面兩篇講了並發編程中線程安全HashMap:ConcurrentHashMap
  • 「原創」Java並發編程系列18|讀寫鎖(下)
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫本文為何適原創並發編程系列第 18 篇,文末有本系列文章匯總。上篇為【原創】Java並發編程系列17 | 讀寫鎖八講(上),沒看過的可以先看看。本文是下篇,從「源碼分析寫鎖的獲取與釋放」開始。7.
  • 「原創」Java並發編程系列25|交換器Exchanger(3)
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫正文很尷尬,發了並發編程的26和27,漏了本篇25。
  • 「原創」Java並發編程系列06|你不知道的final
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫final在Java中是一個保留的關鍵字,可以修飾變量、方法和類。那麼fianl在並發編程中有什麼作用呢?本文就在對final常見應用總結基礎上,講解final並發編程中的應用。
  • 「原創」Java並發編程系列14|AQS源碼分析
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫本文為何適原創並發編程系列第 14 篇,文末有本系列文章匯總。AbstractQueuedSynchronizer是Java並發包java.util.concurrent的核心基礎組件,是實現Lock的基礎。
  • 「零基礎學JAVA」基礎篇 第二章 JAVA編程初體驗
    JAVA【零基礎學編程】系列今天給大家帶來基礎篇 第二章 JAVA編程初體驗本節的部分編碼操作需要先安裝JDK開發工具「零基礎學JAVA」工具篇 JDK的安裝教程(WINDOWS版)和環境變量的配置「零基礎學JAVA」工具篇
  • 「原創」Java並發編程系列29|ConcurrentLinkedQueue
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫J.U.C 為常用的集合提供了並發安全的版本,前面講解了 map 的並發安全集合 ConcurrentHashMap,List 並發安全集合 CopyOnWriteArrayList,Set 並發安全集合