1. 程式人生 > >聊聊併發:(六)指令重排序與happens-before原則分析

聊聊併發:(六)指令重排序與happens-before原則分析

前言

在前幾章中,我們介紹了執行緒安全相關的關鍵字synchronized與volatile的使用,在介紹其原理的過程中,我們提及到了Java中的“happen-before”規則,這個規則對於我們理解Java多執行緒開發非常的有用,本文我們就來了解一下什麼是happen-before規則。

什麼是happen-before?

在上一篇我們介紹volatile實現機制的時候,在指令重排序的部分,我們提及到了happen-before原則,happen-before即先行發生,是Java記憶體模型中定義的兩項操作之間的偏序關係,如果說操作A先行發生於操作B,其實就是說發生操作B之前,操作A產生的影響能被操作B觀察到,“影響”包括修改了記憶體中的共享變數的值、傳送了訊息、呼叫了方法等。

這個原則非常的重要,它是判斷資料是否存在競爭、執行緒是否安全的主要依據,依靠這個原則,我們可以通過幾條規則解決併發環境下兩個操作之間是否可能存在衝突的所有問題。

我們舉一個小例子來看一下:

/**
 * Created by xuanguangyao on 2018/8/20.
 */
public class HappenBeforeDemo {

    //該操作線上程1執行
    public static int i = 1;

    //該操作線上程2執行
    public static int j = i;

    //該操作線上程3執行
    public static int i = 2
; }

上面的例子中,我們聲明瞭兩個靜態類變數,i與j,然後在三條執行緒中操作這兩個變數,假設執行緒1中操作“i=1”先行發生於執行緒2的操作“j=i”,那麼可以確定線上程2的操作執行後,變數j的值一定等於1,得出這個結論的依據有兩個:

一是根據先行發生原則,“i=1”的結果可以被觀察到;

二是執行緒3還沒啟動,執行緒1操作結束之後沒有執行緒會修改變數i的值。

現在再來考慮執行緒3,我們依然保持執行緒1與執行緒2之間的先行發生關係,而執行緒3出現線上程1與執行緒2的操作之間,但是執行緒3與執行緒2沒有先行發生關係,那麼j的值是多少呢?

其實是不確定的,1和2都有可能,因為執行緒3對變數i的影響可能會被執行緒2觀察到,也可能不會,這時候執行緒2就存在讀取到過期資料的風險,不具備多執行緒安全性。

那什麼情況下是多執行緒安全的呢?

當多個執行緒訪問同一個物件時,如果不用考慮這些執行緒在執行時環境下的排程和交替執行,也不需要進行額外的同步,或者在呼叫方進行任何其他的協調操作,呼叫這個物件的行為都可以獲取正確的結果,那這個物件是執行緒安全的。

happen-before規則

下面是Java記憶體模型中的八條可保證happen-before的規則,它們無需任何同步器協助就已經存在,可以在編碼中直接使用。如果兩個操作之間的關係不在此列,並且無法從下列規則推匯出來的話,它們就沒有順序性保障,虛擬機器可以對它們進行隨機地重排序。

  • 程式次序規則:在一個執行緒內,按照程式程式碼順序,書寫在前面的操作先行發生於書寫在後面的操作。準確地說,應該是控制流順序而不是程式程式碼順序,因為要考慮分支、迴圈等結構。
  • 管程鎖定:一個unlock操作先行發生於後面對同一個鎖的lock操作。這裡必須強調的是同一個鎖,而“後面”是指時間上的先後順序。
  • volatile變數規則:對一個volatile變數的寫操作先行發生於後面對這個變數的讀操作,這裡的“後面”同樣是指時間上的先後順序。
  • 執行緒啟動規則:執行緒中的所有操作都先行發生於對此執行緒的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值等手段檢測到執行緒已經終止執行。
  • 執行緒中斷規則:對執行緒interrupt()方法的呼叫先行發生於被中斷執行緒的程式碼檢測到中斷事件的發生,可以通過Thread.interrupt()方法檢測到是否有中斷髮生。
  • 物件終結原則:一個物件的初始化完成(建構函式執行結束)先行發生於它的finalize()方法的開始。
  • 傳遞性:如果操作A先行發生於操作B,操作B先行發生於操作C,那就可以得出操作A先行發生於操作C的結論。

Java中不需要任何同步手段保障就能成立的先行發生規則就是上面這些了,下面我們來看一個例子,如果使用這些規則去判定操作間是否具備順序性,對於讀寫共享變數的操作來說,就是執行緒是否安全。

public class HappenBeforeDemo {

    private int value = 0;

    public void setValue(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }
}    

在上面的例子中,假設存線上程A和B,執行緒A先(是指時間上)呼叫了setValue(1),然後執行緒B呼叫了同一個物件的getValue(),那麼執行緒B收到的返回值是什麼呢?

我們依次分析一下先行發生原則的各項規則,由於兩個方法分別由執行緒A和執行緒B呼叫,不在一個執行緒中,所以第一條程式次序規則不適用;

由於沒有同步塊,自然就不會發生lock和unlock操作,所以管程鎖定規則不適用;

由於value變數沒有被volatile關鍵字修飾,所以volatile變數規則不適用;

後面的執行緒啟動、終止、中斷規則和物件終結規則也和這裡完全沒有關係。

因為沒有一個適用的先行發生規則,所以最後一條傳遞性也無從談起,因此我們可以判定儘管執行緒A在操作時間上先於執行緒B,但是無法確定執行緒B中getValue()方法的返回結果,換句話說,這裡面的操作不是執行緒安全的。

那麼如何解決這個問題呢?我們可以在get/set方法前面加入synchronized關鍵字,這樣可以應用管程鎖定規則;或者把value定義為volatile變數,這樣可以應用volatile變數規則來實現先行發生原則。

通過上面的例子,我們可以得到結論:一個操作“時間上的先發生”不代表這個操作會是“先行發生”,那如果一個操作“先行發生”是否就能推匯出這個操作必定是“時間上的先發生”呢?事實上,也是不可以的,因為CPU指令重排序的存在,我們來看一個小例子:

int i = 1;

int j = 2;

上面的例子中,兩行程式碼在同一個執行緒中執行,根據程式次序規則,第一行程式碼應該先於第二行執行,但是由於兩行程式碼的執行結果沒有前後依賴關係,因此處理器可以對其進行重排序。

結語

我們總結一下,時間先後順序與先行發生原則之間基本沒有太大的關係,所以我們衡量併發安全問題的時候不要受到時間順序的干擾,一切以先行發生原則為準。

本文參考:《深入理解Java虛擬機器》

更多Java乾貨文章請關注我的個人微信公眾號:老宣與你聊Java
這裡寫圖片描述

相關推薦

聊聊併發指令排序happens-before原則分析

前言 在前幾章中,我們介紹了執行緒安全相關的關鍵字synchronized與volatile的使用,在介紹其原理的過程中,我們提及到了Java中的“happen-before”規則,這個規則對於我們理解Java多執行緒開發非常的有用,本文我們就來了解一下什麼是

從單例模式挖到記憶體模型----指令排序

首先是一個雙檢鎖寫的單例模式的例子: public class Single{ private volatile static Single single; private Single(){} public static Single getInstance(){

聊聊併發十一concurrent包之Condition原始碼分析

前言 在前幾篇文章中, 我們介紹了concurrent包中幾種鎖的實現機制,對其原始碼進行了分析,在介紹鎖的文章中,並沒有提及到Condition這個類,其實Condition的使用是與Lock繫結在一起的,本章,我們詳細瞭解一下Conditon的使用方式以及

一個基於JRTPLIB的輕量級RTSP客戶端(myRTSPClient)——實現篇RTP音視頻傳輸解析層之音視頻數據傳輸格式

客戶端 會有 服務 client 基本 cnblogs 存在 額外 導致 一、差異 本地音視頻數據格式和用來傳輸的音視頻數據格式存在些許差異,由於音視頻數據流到達客戶端時,需要考慮數據流的數據邊界、分包、組包順序等問題,所以傳輸中的音視頻數據往往會多一些字節。 舉個例子

Android 開發Activity生命週期以及函式意義

簡述:   1.在日常應用中Activity是與使用者互動的介面,它提供了一個使用者完成相關操作的視窗。當我們在開發中建立Activity後,通過呼叫setContentView(View)方法來給該Activity指定一個佈局介面,而這個介面就是提供給使用者互動的介面。Androi

Java併發程式設計Lock介面

一、Lock介面的引入 由於synchronized關鍵字有些缺陷,如無法響應中斷等,出現了Lock介面。相對於synchronized,Lock有如下補充: Lock可以響應中斷; Lock可以得知執行緒是否已經獲得鎖; Lock可以提供更為複雜的讀寫鎖,以應對讀寫同時存

併發系列-----concurrent的簡單介紹

一 簡介   concurrent包是jdk1.5引入的重要的包,主要程式碼由大牛Doug Lea完成。這個包下的一些類如果用好了可以很方便的保證資料在多執行緒下操作的正確性。就比如說執行緒共享的i++,如果使用concurrent包下的Atomic系列類可以很方便的解決這個問題。

Spring Boot乾貨系列靜態資源和攔截器處理

正文     前面章節我們也有簡單介紹過SpringBoot中對靜態資源的預設支援,今天詳細的來介紹下預設的支援,以及自定義擴充套件如何實現。 預設資源對映 Spring Boot 預設為我們提供了靜態資源處理,使用 WebMvcAutoConfiguration 中

定製併發自定義在計劃的執行緒池內執行的任務

宣告:本文是《 Java 7 Concurrency Cookbook 》的第七章, 作者: Javier Fernández González 譯者:鄭玉婷 自定義在計劃的執行緒池內執行的任務 計劃的執行緒池是 Executor 框架的基本執行緒池的擴充套件,允許你定製一個計劃來執行一段時

併發集合使用執行緒安全的NavigableMap

宣告:本文是《 Java 7 Concurrency Cookbook 》的第六章,作者: Javier Fernández González     譯者:許巧輝 校對:方騰飛 使用執行緒安全的NavigableMap Java API 提供的有趣的資料結構,並且你可以在併發應用程式中使用

Java併發程式設計阻塞佇列

前言 在 Android多執行緒(一)執行緒池這篇文章時,當我們要建立ThreadPoolExecutor的時候需要傳進來一個型別為BlockingQueue的引數,它就是阻塞佇列,在這一篇文章裡我們會介紹阻塞佇列的定義、種類、實現原理以及應用。 1

機器學習系列K-Means聚類

本章,我們介紹了我們的第一個無監督學習方法:聚類。聚類是用來探索無標籤資料的結構的。我們介紹了K-Means聚類演算法,重複將樣本分配的類裡面,不斷的更新類的重心位置。雖然K-Means是無監督學習方法,其效果依然是可以度量的;用畸變程度和輪廓係數可以評估聚類效果。我們用K-Means研究了兩個問題。第一

spark機器學習筆記用Spark Python構建迴歸模型

博主簡介:風雪夜歸子(英文名:Allen),機器學習演算法攻城獅,喜愛鑽研Meachine Learning的黑科技,對Deep Learning和Artificial Intelligence充滿興趣,經常關注Kaggle資料探勘競賽平臺,對資料、Machi

Java併發程式設計的藝術筆記——ConcurentHashMap的原理實現

一.執行緒不安全的HashMap 多執行緒環境下,使用HashMap進行put操作會引起死迴圈(jdk1.7 Entry連結串列形成環形資料結構),導致CPU利用率接近100%。 二.效率低下的HashTable 多個執行緒訪問HashTable的同步方法,會引起阻塞或輪詢狀態。 三.Concurre

Spring Cloud之路RabbitMQ初探

一、RabbitMQ介紹 RabbitMQ 即一個訊息佇列,主要是用來實現應用程式的非同步和解耦,同時也能起到訊息緩衝,訊息分發的作用。 訊息中介軟體在網際網路公司的使用中越來越多,剛才還看到新聞阿里將RocketMQ捐獻給了apache,當然了今天的主角還是講RabbitMQ。

JAVA 併發 ReentrantLock 原始碼分析

ReentrantLock實現了Lock介面,是一種遞迴無阻塞的同步機制。 首先ReentrantLock有公平鎖和非公平鎖機制,使用構造方法ReentrantLock(boolean)指定,預設是非公平鎖。 公平鎖的含義是執行緒按照發出請求的先後順序獲取鎖

java併發程式設計之讀寫鎖

一、讀寫鎖我們知道在多個執行緒訪問同一個資料的時候是存線上程安全問題的,而在僅僅是讀取資料的時候,是沒有安全問題的,那麼多個執行緒同時讀取資料我們就可以讓其不互斥;而多個執行緒都在修改(寫)資料或有的在讀取有的在寫入的時候再讓其互斥,這樣不但保證執行緒安全而且提高效能。Rea

Java併發程式設計-Phaser

phaser         英文意思移相器,相位器,表示“階段器”,用來解決控制多個執行緒分階段共同完成任務的情景問題,其作用相比CountDownLatch和CyclicBarrier更加靈活。如100個人參加高考需要考四場考試,早上考語文,需要等100人都

IT輪子系列——Excel上傳解析,一套代碼解決所有Excel業務上傳,你Get到了嗎

tryparse mappath src 個推 列名 import ges bject tab 前言 在日常開發當中,excel的上傳與解析是很常見的。根據業務不同,解析的數據模型也都不一樣。不同的數據模型也就需要不同的校驗邏輯,這往往需要寫多套的代碼進行字段的檢驗,如必填

Docker學習: 網絡使用配置

sock AR -i 回顧 覆蓋 htm 參考 ble dock 特別聲明:   博文主要是學習過程中的知識整理,以便之後的查閱回顧。部分內容來源於網絡(如有摘錄未標註請指出)。內容如有差錯,也歡迎指正! =============系列文章============= 1