天天看點

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會是多少呢?

Copy

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。這就是傳遞性的規則。我們再來看看剛才那段代碼(我複制下來友善看)

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 = true,this.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;

那麼此時的f.x == 0。是以在構造函數中沒有逸出,那麼final修飾的字段沒有問題。詳情的案例可以參考這個文檔

總結#

在這篇文章當中,我一開始對于文章最後部分的final限制重排一直看的不懂。網上不斷地搜尋資料和看文章當中提供的資料我才慢慢看懂,反複看了不下十遍。可能腦子不太靈活吧。

該文章主要的核心内容就是Happens-Before規則,把這幾條規則搞懂了就ok。

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

個人部落格網址: 

https://colablog.cn/

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

作者: Johnson木木

出處:

https://www.cnblogs.com/Johnson-lin/p/12736004.html