1. 程式人生 > >Java併發程式設計實戰 02Java如何解決可見性和有序性問題

Java併發程式設計實戰 02Java如何解決可見性和有序性問題

摘要

在上一篇文章當中,講到了CPU快取導致可見性、執行緒切換導致了原子性、編譯優化導致了有序性問題。那麼這篇文章就先解決其中的可見性和有序性問題,引出了今天的主角:Java記憶體模型(面試併發的時候會經常考核到)

什麼是Java記憶體模型?

現在知道了CPU快取導致可見性、編譯優化導致了有序性問題,那麼最簡單的方式就是直接禁用CPU快取和編譯優化。但是這樣做我們的效能可就要爆炸了~。我們應該按需禁用。
Java記憶體模型是有一個很複雜的規範,但是站在程式設計師的角度上可以理解為:Java記憶體模型規範了JVM如何提供按需禁用快取和編譯優化的方法。
具體包括 volatile、synchronized、final三個關鍵字,以及六項Happens-Before規則。

volatile關鍵字

volatile有禁用CPU快取的意思,禁用CPU快取那麼操作資料變數時直接是直接從記憶體中讀取和寫入。如:使用volatile宣告變數 volatile boolean v = false,那麼操作變數v時則必須從記憶體中讀取或寫入,但是在低於Java版本1.5以前,可能會有問題。
在下面這段程式碼當中,假設執行緒A執行了write方法,執行緒B執行了reader方法,假設執行緒B判斷到了this.v == true進入到了判斷條件中,那麼此時的x會是多少呢?

public class VolatileExample {
    private int x = 0;
    private volatile boolean v = false;

    public void write() {
        this.x = 666;
        this.v = true;
    }

    public void reader() {
        if (this.v == true) {
            // 這裡的x會是多少呢?
        }
    }
}

在1.5版本之前,該值可能為666,也可能為0;因為變數x並沒有禁用快取(volatile),但是在1.5版本以後,該值一定為666;因為Happens-Before規則。

什麼是Happens-Before規則

Happens-Before規則要表達的是:前面一個操作的結果對後續是可見的。如果第一次接觸該規則,可能會有一些困惑,但是多去閱讀幾遍,就會加深理解。

1.程式的順序性規則

這條規則是指在一個執行緒中,按照程式順序,前面的操作Happens-Before於後續的任意操作(意思就是前面的操作結果對於後續任意操作都是可以看到的)。就如上面的那段程式碼,按照程式的順序:this.x = 666

Happens-Before於 this.v = true

2.Volatile 變數規則

這條規則指的是對一個Volatile變數的寫操作,Happens-Before該變數的讀操作。意思也就是:假設該變數被執行緒A寫入後,那麼該變數對於任何執行緒都是可見的。也就是禁用了CPU快取的意思,如果是這樣的話,那麼和1.5版本以前沒什麼區別啊!那麼如果再看一下規則3,就不同了。

3.傳遞性

這條規則指的是:如果 A Happens-Before 於B,且 B Happens-Before 於 C。那麼 A Happens-Before 於 C。這就是傳遞性的規則。我們再來看看剛才那段程式碼(我複製下來方便看)

public class VolatileExample {
    private int x = 0;
    private volatile boolean v = false;

    public void write() {
        this.x = 666;
        this.v = true;
    }

    public void reader() {
        if (this.v == true) {
            // 讀取變數x
        }
    }
}

在上面程式碼,我們可以看到,this.x = 666 Happens-Before this.v = truethis.v = true Happens-Before 讀取變數x,根據傳遞性規則this.x = 666 Happens-Befote 讀取變數x,那麼說明了讀取到變數this.v = true時,那麼此時的讀取變數x的指必定為666
假設執行緒A執行了write方法,執行緒B執行reader方法且此時的this.v == true,那麼根據剛才所說的傳遞性規則,讀取到的變數x必定為666。這就是1.5版本對volatile語義的增強。而如果在版本1.5之前,因為變數x並沒有禁用快取(volatile),所以變數x可能為0哦。

4.管程中鎖的規則

這條規則是指對一個鎖的解鎖操作 Happens-Before 於後續對這個鎖的加鎖操作。管程是一種通用的同步原語,在Java中,synchronized是Java裡對管程的實現。
管程中的鎖在Java裡是隱式實現的。如下面的程式碼,在進入同步程式碼塊前,會自動加鎖,而在程式碼塊執行完後會自動解鎖。這裡的加鎖和解鎖都是編譯器幫我們實現的。

synchronized(this) { // 此處自動加鎖
   // x是共享變數,初始值 = 0
   if (this.x < 12) {
      this.x = 12;
   }
} // 此處自動解鎖

結合管程中的鎖規則,假設x的初始值為0,執行緒A執行完程式碼塊後值會變成12,那麼當執行緒A解鎖後,執行緒B獲取到鎖進入到程式碼塊後,就能看到執行緒A的執行結果x = 12。這就是管程中鎖的規則

5.執行緒的start()規則

這條規則是關於執行緒啟動的,該規則指的是主執行緒A啟動子執行緒B後,子執行緒B能夠看到主執行緒啟動子執行緒B前的操作。
用HappensBefore解釋:執行緒A呼叫執行緒B的start方法 Happens-Before 執行緒B中的任意操作。參考程式碼如下:

    int x = 0;
    public void start() {
        Thread thread = new Thread(() -> {
            System.out.println(this.x);
        });

        this.x = 666;
        // 主執行緒啟動子執行緒
        thread.start();
    }

此時在子執行緒中列印的變數x值為666,你也可以嘗試一下。

6.執行緒join()規則

這條規則是關於執行緒等待的,該規則指的是主執行緒A等待子執行緒B完成(主線A通過呼叫子執行緒B的join()方法實現),當子執行緒B完成後,主執行緒能夠看到子執行緒的操作,這裡的看到指的是共享變數 的操作,用Happens-Before解釋:如果線上程A中呼叫了子執行緒B的join()方法併成功返回,那麼子執行緒B的任意操作 Happens-Before 於主執行緒呼叫子執行緒Bjoin()方法的後續操作。看程式碼比較容易理解,示例程式碼如下:

    int x = 0;
    public void start() {
        Thread thread = new Thread(() -> {
            this.x = 666;
        });
        // 主執行緒啟動子執行緒
        thread.start();
        // 主執行緒呼叫子執行緒的join方法進行等待
        thread.join();
        // 此時的共享變數 x == 666
    }

被忽略的final

在1.5版本之前,除了值不可改變以外,final欄位其實和普通的欄位一樣。
在1.5以後的Java記憶體模型中,對final型別變數重排進行了約束。現在只要我們的提供正確的建構函式沒有逸出,那麼在建構函式初始化的final欄位的最新值,必定可以被其他執行緒所看到。程式碼如下:

class FinalFieldExample {
  final int x;
  int y;
  static FinalFieldExample f;
  public FinalFieldExample() {
    x = 3;
    y = 4;
  }

  static void writer() {
    f = new FinalFieldExample();
  }

  static void reader() {
    if (f != null) {
      int i = f.x;
      int j = f.y;
    }
  }

當執行緒執行reader()方法,並且f != null時,那麼此時的final欄位修飾的f.x 必定為 3,但是y不能保證為4,因為它不是final的。如果這是在1.5版本之前,那麼f.x也是不能保證為3
那麼何為逸出呢?我們修改一下建構函式:

 public FinalFieldExample() {
   x = 3;
   y = 4;
   // 此處為逸出
   f = this;
 }

這裡就不能保證 f.x == 3了,就算x變數是用final修飾的,為什麼呢?因為在建構函式中可能會發生指令重排,執行變成下面這樣:

    // 此處為逸出
   f = this;
   x = 3;
   y = 4;

那麼此時的f.x == 0。所以在建構函式中沒有逸出,那麼final修飾的欄位沒有問題。詳情的案例可以參考這個文件

總結

在這篇文章當中,我一開始對於文章最後部分的final約束重排一直看的不懂。網上不斷地搜尋資料和看文章當中提供的資料我才慢慢看懂,反覆看了不下十遍。可能腦子不太靈活吧。
該文章主要的核心內容就是Happens-Before規則,把這幾條規則搞懂了就ok。

參考文章:極客時間:Java併發程式設計實戰 02

個人部落格網址: https://colablog.cn/

如果我的文章幫助到您,可以關注我的微信公眾號,第一時間分享文章給您

相關推薦

JAVA併發程式設計:三大特性-可見有序性

生活 生活就是生下來,活下去。 ———— 在JAVA併發程式設計,如果要保證程式的執行緒安全,就要保證程式碼的原子性、可見性、有序性。 昨天聊了原子性。今天來看下可見性、有序性。 什麼是可見性? 當多個執行緒訪問同一個變數時,一個執行緒修改了一個變數的值,其他執行緒

Java併發程式設計實戰 02Java如何解決可見有序性問題

摘要 在上一篇文章當中,講到了CPU快取導致可見性、執行緒切換導致了原子性、編譯優化導致了有序性問題。那麼這篇文章就先解決其中的可見性和有序性問題,引出了今天的主角:Java記憶體模型(面試併發的時候會經常考核到) 什麼是Java記憶體模型? 現在知道了CPU快取導致可見性、編譯優化導致了有序性問題,那麼最簡

JAVA併發程式設計實戰》避免活躍危險

文章目錄 死鎖 鎖順序死鎖 動態的鎖順序死鎖 在協作物件之間發生的死鎖 開放呼叫 資源死鎖 死鎖的避免和診斷 支援定時的鎖 使用執行緒轉儲資訊來分析死鎖 其他活躍性危

java併發程式設計實戰:避免活躍危險筆記

死鎖 鎖順序死鎖 簡單的鎖順序死鎖示例: public class LeftRightDeadlock { private final Object left = new Object(); private final Object right = ne

Java併發程式設計實踐筆記之—可見(Visibility)

同步的重要性有兩個方面: 實現原子性:防止某個執行緒正在使用物件狀態而另一個執行緒同時在修改改狀態記憶體可見性:確保一個執行緒修改了物件狀態後,其他執行緒能夠看到發生的狀態變化失效資料 缺乏同步的程式可能會產生的一種錯誤情況就是——失效資料失效資料舉例//在沒有同步的情況下共享資料 public class

【漫畫】JAVA併發程式設計三大Bug源頭(可見、原子有序性)

> 原創宣告:本文轉載自公眾號【胖滾豬學程式設計】​ 某日,胖滾豬寫的程式碼導致了一個生產bug,奮戰到凌晨三點依舊沒有解決問題。胖滾熊一看,只用了一個volatile就解決了。並告知胖滾豬,這是併發程式設計導致的坑。這讓胖滾豬堅定了要學好併發程式設計的決心。。於是,開始了我們併發程式設計的第一課。

Java併發程式設計實戰 03互斥鎖 解決原子問題

# 文章系列 [Java併發程式設計實戰 01併發程式設計的Bug源頭](https://mp.weixin.qq.com/s/QT44HS47l_ir08pCZeFU5Q) [Java併發程式設計實戰 02Java如何解決可見性和有序性問題](https://mp.weixin.qq.com/s/Ryu

併發程式設計實戰(2):原子可見競態條件與複合操作

原子性 一個不可分割的操作,比如a=0;再比如:a++; 這個操作實際是a = a + 1;是可分割的,它其實包含三個獨立的操作:讀取a的值,將值加1,然後將計算結果寫入a,這是一個“讀取-修改-寫入”的操作序列,所以他不是一個原子操作。 可見性 可見性,是指執行緒之間的可見

Java複習-併發程式設計中的三個問題:原子可見有序性

在併發程式設計中,我們通常會遇到以下三個問題:原子性問題,可見性問題,有序性問題。 1、原子性: 原子性:即一個操作或者多個操作 要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行。 2、可

Java併發基礎】併發程式設計bug源頭:可見、原子有序性

前言 CPU 、記憶體、I/O裝置之間的速度差距十分大,為了提高CPU的利用率並且平衡它們的速度差異。計算機體系結構、作業系統和編譯程式都做出了改進: CPU增加了快取,用於平衡和記憶體之間的速度差異。 作業系統增加了程序、執行緒,以時分複用CPU,進而均衡CPU與I/O裝置之間的速度差異。 編譯程式優化指

JAVA併發程式設計實戰》取消關閉

文章目錄 引言 任務取消 中斷 中斷策略 響應中斷 示例:計時執行 通過Future來實現取消 處理不可中斷的阻塞 採用newTaskFor封裝非標準的取消 停止基於執行緒的服務

JAVA併發程式設計實戰》任務執行

文章目錄 線上程中執行任務 序列執行任務 顯式的為任務建立執行緒 無限制建立執行緒的不足 Executor框架 示例:基於Executor的Web伺服器 執行策略 執行緒池 Exe

JAVA併發程式設計實戰》基礎構建模組

文章目錄 同步容器類 同步容器類的問題 迭代器和ConcurrentModificationException 隱藏迭代器 併發容器 ConcurrentHashMap 額外的原子Map操作

JAVA併發程式設計實戰》物件的組合

文章目錄 設計執行緒安全的類 找出構成物件狀態的所有變數 示例 找出約束狀態變數的不變性條件 例項封閉 java監視器模式 示例:車輛追蹤 執行緒安全性的委託

java併發程式設計實戰】—–執行緒基本概念

轉自 http://cmsblogs.com/?p=1638 共享和可變 要編寫執行緒安全的程式碼,其核心在於對共享的和可變的狀態進行訪問。 “共享”就意味著變數可以被多個執行緒同時訪問。我們知道系統中的資源是有限的,不同的執行緒對資源都是具有著同等的使用權。有限、公平就意味著競爭

讀書筆記(java併發程式設計實戰——CompletionService)

原文請參考微信公眾號(歡迎關注公眾號:coding_song):https://mp.weixin.qq.com/s/R50Eh4kTDtA031i-yMUZAw    Callable&Future Callbale描述的是抽象的計算任務,有明確的起點,並且最終會結束

Java併發程式設計實戰 - 學習筆記

第2章 執行緒安全性 1. 基本概念 什麼是執行緒安全性?可以這樣理解:一個類在多執行緒環境下,無論執行時環境怎樣排程,無論多個執行緒之間的執行順序是什麼,且在主調程式碼中不需要進行任何額外的同步,如果該類都能呈現出預期的、正確的行為,那麼該類就是執行緒安全的。 既然這樣,那麼安

Java併發程式設計實戰:閉鎖CountDownLatch,柵欄CyclicBarrier與訊號量Semaphore

整體上對三個概念進行一個說明: CountDownLatch和CyclicBarrier都能夠實現執行緒之間的等待,只不過它們側重點不同: CountDownLatch是閉鎖,相當於一扇門:在閉鎖達到結束狀態之前,這扇門一直是關閉的,並且沒有任何執行緒能夠通過,當到達結束

Java併發程式設計實戰————Semaphore訊號量的使用淺析

引言 本篇部落格講解《Java併發程式設計實戰》中的同步工具類:訊號量 的使用和理解。 從概念、含義入手,突出重點,配以程式碼例項及講解,並以生活中的案例做類比加強記憶。 什麼是訊號量 Java中的同步工具類訊號量即計數訊號量(Counting Semaphore),是

Java併發程式設計實戰————物件的組合

引言 物件的組合,是《Java Concurrency in Practice》中第四章引入的課題。這並不是一個併發的概念。 為了可以將現有的執行緒安全元件組合為更大規模的元件或程式,而不是每次記憶體訪問都進行分析以確保程式是執行緒安全的。這一章將介紹一些組合模式,這些模式可以更容易的使