Java多線程以內存可見性

什麼叫「可見性」?java

一個線程對共享變量值的修改,可以被其餘線程及時看到。緩存

共享變量:若是一個變量在多個線程的工做內存中存在副本,那麼這個變量就是這幾個線程的共享變量。安全

全部變臉都存在主內存中,每一個線程都有本身獨立的工做內存,裏面保存該線程使用大的變量副本,關係以下圖所示:多線程

 

多線程遵照的兩條規定eclipse

1.線程對共享變量全部的操做都只能在本身的工做內存中完成,沒法直接從主內存中讀寫ide

2.不一樣線程之間沒法訪問其餘線程中的變量,線程中變量值的傳遞須要經過主內存來完成。性能

 

共享變量可見性的實現原理優化

線程1對共享變量的修改若是要被線程2及時看到,須要通過2個步驟:this

1.把工做內存1中更新過的共享變量值刷新到主內存中spa

2.把主內存中最新的共享變量的值更新打工做內存2中

以上2個步驟,任意一個出現問題,都會致使共享變量沒法被其餘線程及時看到,沒法實現可見性,致使其餘線程讀取的數據不許確從而產生線程不安全。

 

共享變量可見性的實現方式

Java語言層面支持的可見性實現方式有2種,分別是synchronizedvolatile

synchronized:可以實現原子性(同步)和可見性

volatile:可以保證可見性,可是沒法保證原子性

 

synchronized是如何實現可見性?

java內存模型(JMM)中關於synchronized的兩條規定:

1).線程解鎖前,必須把共享變量的最新值刷新到主內存中

2).線程加鎖時,將清空工做內存中共享變量的值,從而使用共享變量時須要從主內存中從新讀取最新值(注意:加鎖與解鎖須要是同一把鎖)

 

線程執行互斥代碼的過程

1.得到互斥鎖

2.清空工做內存

3.從主內存拷貝變量的最新副本到工做內存中

4.執行代碼

5.將更改後的共享變量值刷新到主內存中

6.釋放互斥鎖

 

指令重排序

代碼書寫的順序與實際執行的順序不一樣,指令重排序是編譯器或者處理器爲了提升程序性能而作的優化。

目前的指令從排序有3種方式:

1.編譯器優化的重排序(編譯器優化)

2.處理器優化的重排序(處理器優化)

3.內存優化的重排序(處理器優化)

 

as-if-serial

不管如何重排序,程序執行的結果都應該與代碼順序執行的結果一致(java編譯器和處理器運行時都會保證在單線程中遵循as-if-serial規則,多線程存在程序交錯執行時,則不遵照)

舉例:

int num1 = 1;

int num2 = 2;

int num3 = num1 + num2;

上面3行代碼,在單線程時,第一、2行能夠進行重排序,可是第3行不能夠,不然結果將不同,因此從排序不會給單線程帶來內存可見性的問題。

而在多線程中,程序交錯執行時,重排序則會形成內存可見性的問題。

 

Synchronized實現可見性的代碼,如下的這個類SynchronizedDemo 

public class SynchronizedDemo {
    // 共享變量
    private boolean ready  = false;
    private int     num    = 1;
    private int     result = 0;

    // 寫操做
    public void write() {
        ready = true; // 1.1
        num = 2; // 1.2
    }

    // 讀操做
    public void read() {
        if (ready) { // 2.1
            result = num * 3; // 2.2
        }
        System.out.println("result = " + result);
    }
    
    private class ReadWriteThread extends Thread {
        private boolean flag;

        public ReadWriteThread(boolean flag) {
            this.flag = flag;
        }

        @Override
        public void run() {
            if (flag) {
                write();
            } else {
                read();
            }
        }
    }

    public static void main(String[] args) {
        SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
        synchronizedDemo.new ReadWriteThread(false).start();
        synchronizedDemo.new ReadWriteThread(true).start();
    }

}

上面的這一段代碼重排序後的執行順序多是

1. 1.2-->2.1-->2.2-->1.1;  result=0

2. 1.1-->2.1-->2.2-->1.2;  result=3

......

致使共享變量在線程之間不可見的緣由

1.線程的交叉執行

2.重排序結合線程交叉執行

3.共享變量更新後的值,沒有在工做內存與主內存間及時刷新

 

安全的代碼,加入synchronized關鍵字

    // 寫操做
    public synchronized void write() {
        ready = true; // 1.1
        num = 3; // 1.2
    }

    // 讀操做
    public synchronized void read() {
        if (ready) { // 2.1
            result = num * 2; // 2.2
        }
        System.out.println("result = " + result);
    }

 

volatile是如何實現可見性?

深刻來講,是經過加入內存屏障和禁止重排序優化來實現的。

對volatile變量執行寫操做時,會在寫操做後加入一條store屏障指令,會將cup數據強制刷新到主內存中去

對volatile變量執行讀操做時,會在讀操做前加入一條load屏障指令,強制緩存器中的緩存失效,每次使用都要去主內存中從新獲取數據

通俗地講,volatile變量在每次被訪問的時候,都強迫從主內存中讀取該變量的值,而當該變量在發生變化時,又會強迫變量講最新的值刷新到主內存中,這樣,任意時刻,不一樣的線程總能看到該變量的最新值。

 

線程寫volatile變量的過程:

1.改變線程工做內存中volatile變量副本的值

2.將改變的副本的值從工做內存中刷新到主內存中

 

線程讀volatile變量的過程:

1.從主內存中讀取volatile變量的最新值到工做內存中

2.從工做內存中讀取volatile變量的副本

 

volatile不能保證原子性,請看下面的代碼:

public class VolatileDemo {
    private volatile int num = 0;

    public int getNum() {
        return this.num;
    }

    public void increase() {
        // num++,不是原子操做,這裏會先讀取,再加1
        this.num++;
    }

    public static void main(String[] args) {
        final VolatileDemo volatileDemo = new VolatileDemo();
        // 建立500個子線程,執行increase方法,每次都讓num加1
        for (int i = 0; i < 500; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    volatileDemo.increase();
                }
            }).start();
        }
        // 等到全部子線程執行完畢,eclipse這裏是1,IntelliJ IDEA執行用戶代碼的時候,實際是經過反射方式去調用,而與此同時會建立一個Monitor Ctrl-Break 用於監控目的,全部是2
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        // 因爲num使用了volatile關鍵字,因此預期值應該是500
        System.out.println("當前的num值=" + volatileDemo.getNum());
    }
}

執行後,會發現,有時候不是500,而是499或者498或者497等等,緣由是num++不是原子操做,volatile只能保證變量修改後的可見性,可是沒法保證原子性,請看下面的步驟:

假設如今num=5

1.線程A讀取num的值,線程A的工做內存中,num=5

2.線程B也讀取了num的值,線程B的工做內存中,num=5

3.線程B進行加1操做,線程B的工做內存中,num=6

4.線程B寫入最新的num值,主線程中num的值變爲6

5.線程A執行加1操做,線程A的工做內存中,num=6

6.線程A寫入最新的num值,主線程中num的值變爲6

這樣,兩個線程各自執行了一次加1操做,可是主線程中的數據num=6,這就是因爲volatile沒辦法保證代碼的原子性,使得讀和寫不是一塊兒的

解決方案:

1.使用synchronized關鍵字

2.使用ReentrantLock

3.使用AtomicInteger

 

volatile的適用場景

1.對變量的寫入操做不依賴其當前值

  不知足:num++、count = count * 5

  知足:boolean值變量,記錄溫度變化的變量等等

2.該變量沒有包含在具備其餘變量的不變式中

  不知足:low < up

通常的應用場景不少會不知足其中一個,因此volatile是使用沒喲synchronized這麼普遍。

 

synchronized與volatile比較

1.volatile不須要加鎖,比synchronized更輕量級,不會阻塞線程

2.從內存的角度,volatile讀操做至關於加鎖,寫操做至關於解鎖

3.synchronized既能保證原子性又能保證可見性,而volatile只能保證可見性沒法保證原子性

相關文章
相關標籤/搜索