天天看點

Volatile及JMM

參考:

https://blog.csdn.net/suyimin2010/article/details/80722262

https://www.cnblogs.com/yuluoxingkong/p/9236077.html

https://blog.csdn.net/sdr_zd/article/details/81323519

https://www.jianshu.com/p/2ab5e3d7e510

1.valatile的了解

[1].valatile是java虛拟機提供的輕量級的同步機制

①.保證可見性(當有線程改變了主記憶體中的資料,其他線程馬上能收到變動)

package com.w4xj.interview.thread;

import java.util.concurrent.TimeUnit;

public class VolatileTest {

    public static void main(String[] args) {

        Data data = new Data();

        new Thread(() ->{

            System.out.println(Thread.currentThread().getName() + "\t AAA thread begin");

            try {

                TimeUnit.SECONDS.sleep(3);

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

            data.setNum();

            System.out.println(Thread.currentThread().getName() + "\t updated numer value: " + data.num);

        },"AAA").start();

        while (data.num == 0){}

        System.out.println(Thread.currentThread().getName() + "\t update is over, the num is " + data.num);

    }

}

class Data{

    //volatile 關鍵字就可以保證可見性,若不加這個關鍵字,主線程是收不到AAA線程修改num變量成功的通知的

    volatile int num = 0;

    public void setNum(){

        this.num = 66;

    }

}

②.不保證原子性

package com.w4xj.interview.thread;

import java.util.concurrent.TimeUnit;

public class VolatileTest {

    public static void main(String[] args) {

        Data data = new Data();

        for (int i = 0; i < 20; i++) {

            new Thread(() -> {

                for (int j = 0; j < 100; j++) {

                    data.plus();

                }

            }, String.valueOf(i)).start();

        }

        //2即代表gc線程和main線程

        while (Thread.activeCount() > 2){

            //讓出線程執行權

            Thread.yield();

        }

        //輸出總是小于2000

        System.out.println(Thread.currentThread().getName() +" : "+ data.num);

    }

}

class Data {

    //volatile 關鍵字就可以保證可見性,若不加這個關鍵字,主線程是收不到AAA線程修改num變量成功的通知的

    volatile int num = 0;

    public void setNum() {

        this.num = 66;

    }

    public void plus() {

        //這裡睡1毫秒,讓這個加塞出現的更明顯一些,因為自增這個操作太快了,同樣也是線程速度過快,會導緻可見性來不及通知是以導緻寫被覆寫

        try {

            Thread.currentThread().sleep(1);

        } catch (InterruptedException e) {

            e.printStackTrace();

        }

        num++;

    }

}

③.禁止指令重排(保證有序性)

2.JMM

[1].介紹

①.java記憶體模型(即Java Memory Model,簡稱JMM)本身是一種抽象的概念,并不真實存在,它描述的是一組規則或規範,通過這組規範定義了程式中各個變量(包括執行個體字段,靜态字段和構成數組對象的元素)的通路方式。

②.JMM關于同步的規定:

線程解鎖前,必須把共享變量的值重新整理回主記憶體

線程加鎖前,必須讀取主記憶體的最新值到自己的工作記憶體

加鎖解鎖是同一把鎖

③.由于JVM運作程式的實體是線程,而每個線程建立時JVM都會為其建立一個工作記憶體(有些地方稱為棧空間),用于存儲線程私有的資料,而Java記憶體模型中規定所有變量都存儲在主記憶體,主記憶體是共享記憶體區域,所有線程都可以通路,但線程對變量的操作(讀取指派等)必須在工作記憶體中進行,首先要将變量從主記憶體拷貝的自己的工作記憶體空間,然後對變量進行操作,操作完成後再将變量寫回主記憶體,不能直接操作主記憶體中的變量,工作記憶體中存儲着主記憶體中的變量副本拷貝,前面說過,工作記憶體是每個線程的私有資料區域,是以不同的線程間無法通路對方的工作記憶體,線程間的通信(傳值)必須通過主記憶體來完成,其簡要通路過程如下圖

Volatile及JMM

[2].可見性

各個線程對主記憶體中共享變量的操作都是各個線程各自拷貝到自己的工作的工作記憶體進行讀寫操作後在寫到主記憶體中的,這就可能存在一個線程AAA修改了共享變量num的值但還未寫回主記憶體時,另外一個線程BBB又對主記憶體中同一個變量num進行操作,但此時A線程工作記憶體中共享變量num對線程B來說并不可見,這種工作記憶體與主存同步延遲的現象就造成了可見性問題

[3].原子性

①.為什麼num++在多線程下是非線程安全的

Volatile及JMM

雖然++操作從源碼上看是一條指令,但實際從位元組碼來看是4步,是以加了volatile仍然無法保證原子性

②.解決方式1:synchronized解決(性能低)

③.采用原子類:原子類是juc(java.util.concurrent)下面的保證原子性的包裝類

Volatile及JMM

package com.w4xj.interview.thread;

import java.util.concurrent.TimeUnit;

import java.util.concurrent.atomic.AtomicInteger;

public class VolatileTest {

    public static void main(String[] args) {

        Data data = new Data();

        for (int i = 0; i < 20; i++) {

            new Thread(() -> {

                for (int j = 0; j < 100; j++) {

                    data.plus();

                    data.plusAtomic();

                }

            }, String.valueOf(i)).start();

        }

        //2即代表gc線程和main線程

        while (Thread.activeCount() > 2){

            //讓出線程執行權

            Thread.yield();

        }

        System.out.println(Thread.currentThread().getName() +" : "+ data.num);

        System.out.println(Thread.currentThread().getName() +" : "+ data.atomicInteger);

    }

}

class Data {

    //volatile 關鍵字就可以保證可見性,若不加這個關鍵字,主線程是收不到AAA線程修改num變量成功的通知的

    volatile int num = 0;

    //帶原子性的Integer

    AtomicInteger atomicInteger = new AtomicInteger();

    public void setNum() {

        this.num = 66;

    }

    public void plus() {

        //這裡睡1毫秒,讓這個加塞出現的更明顯一些,因為自增這個操作太快了,同樣也是線程速度過快,會導緻可見性來不及通知是以導緻寫被覆寫

        try {

            Thread.currentThread().sleep(1);

        } catch (InterruptedException e) {

            e.printStackTrace();

        }

        num++;

    }

    public void plusAtomic() {

        try {

            Thread.currentThread().sleep(1);

        } catch (InterruptedException e) {

            e.printStackTrace();

        }

        atomicInteger.getAndIncrement();

    }

}

[4].有序性

①.計算機在執行程式時,為了提高性能,編譯器和處理器的常常會對指令做重排,一般分以下3種:

Volatile及JMM

a.編譯器優化的重排序:

編譯器在不改變單線程程式語義的前提下,可以重新安排語句的執行順序。【as-if-serial原則保證,as-if-serial語義:不管怎麼重排序(編譯器和處理器為了提高并行度),(單線程)程式的執行結果不能被改變。】

b.指令級并行的重排序:

現代處理器采用了指令級并行技術來将多條指令重疊執行。如果不存在資料依賴性(即後一個執行的語句無需依賴前面執行的語句的結果),處理器可以改變語句對應的機器指令的執行順序。

int x = 1;//步驟1

int y = 2;//步驟2

x = x + 1;//步驟3

y = x + x;//步驟4

重排後可能的順序:

1234

1324

2134

4***肯定是不行的,必須準守資料依賴

c.記憶體系統重排序:

由于處理器使用緩存和讀寫緩存沖區,這使得加載(load)和存儲(store)操作看上去可能是在亂序執行,因為三級緩存的存在,導緻記憶體與緩存的資料同步存在時間差。兩個線程中使用的變量能否保證一緻性無法确定。

int x,y,a,b=0;

線程1 線程2
x = a; y = b;
b = 1; a = 2;
x = 0 y = 0

指定重排後

線程1 線程2
b = 1; a = 2;
x = a; y = b;
x = 2  y = 1

②.volatile禁止指令重排原理

a.volatile關鍵字另一個作用就是禁止指令重排優化,進而避免多線程環境下程式出現亂序執行的現象,關于指令重排優化前面已詳細分析過,這裡主要簡單說明一下volatile是如何實作禁止指令重排優化的。

b.先了解一個概念,記憶體屏障(Memory Barrier)。記憶體屏障,又稱記憶體栅欄,是一個CPU指令,它的作用有兩個

i.一是保證特定操作的執行順序,

ii.二是保證某些變量的記憶體可見性(利用該特性實作volatile的記憶體可見性)。

c.硬體層的記憶體屏障分為兩種:Load Barrier 和 Store Barrier即讀屏障和寫屏障

d.java記憶體屏障主要4種

LoadLoad屏障:對于這樣的語句Load1; LoadLoad; Load2,在Load2及後續讀取操作要讀取的資料被通路前,保證Load1要讀取的資料被讀取完畢。

StoreStore屏障:對于這樣的語句Store1; StoreStore; Store2,在Store2及後續寫入操作執行前,保證Store1的寫入操作對其它處理器可見。

LoadStore屏障:對于這樣的語句Load1; LoadStore; Store2,在Store2及後續寫入操作被刷出前,保證Load1要讀取的資料被讀取完畢。

StoreLoad屏障:對于這樣的語句Store1; StoreLoad; Load2,在Load2及後續所有讀取操作執行前,保證Store1的寫入對所有處理器可見。它的開銷是四種屏障中最大的。在大多數處理器的實作中,這個屏障是個萬能屏障,兼具其它三種記憶體屏障的功能

e.由于編譯器和處理器都能執行指令重排優化。如果在指令間插入一條Memory Barrier則會告訴編譯器和CPU,不管什麼指令都不能和這條Memory Barrier指令重排序,也就是說通過插入記憶體屏障禁止在記憶體屏障前後的指令執行重排序優化。Memory Barrier的另外一個作用是強制刷出各種CPU的緩存資料,是以任何CPU上的線程都能讀取到這些資料的最新版本。總之,volatile變量正是通過記憶體屏障實作其在記憶體中的語義,即可見性和禁止重排優化。

對Volatile變量進行讀操作時,會在讀操作前加入一條load屏障指令,從主記憶體中讀取共享變量 對Volatile變量進行寫操作時,會在寫操作後加入一條store屏障指令,将工作記憶體中的共享變量值重新整理回到主記憶體
Volatile及JMM
Volatile及JMM

③.指令重排經典案例之DCL,這裡不讨論sysnchronized

a.代碼

package com.w4xj.interview.thread;

public class DoubleCheckLock {

    private static DoubleCheckLock instance;

    private DoubleCheckLock(){}

    public static DoubleCheckLock getInstance(){

        //第一次檢測

        if (instance == null){

            //同步

            synchronized (DoubleCheckLock.class){

                if (instance == null){

                    //多線程環境下可能會出現問題的地方

                    instance = new DoubleCheckLock();

                }

            }

        }

        return instance;

    }

}

c.上述代碼一個經典的單例的雙重檢測的代碼,這段代碼在單線程環境下并沒有什麼問題,但如果在多線程環境下就可以出現線程安全問題。原因在于某一個線程執行到第一次檢測,讀取到的instance不為null時,instance的引用對象可能沒有完成初始化。因為instance = new DoubleCheckLock();可以分為以下3步完成(僞代碼)

memory = allocate(); //1.配置設定對象記憶體空間

instance = memory;   //3.設定instance指向剛配置設定的記憶體位址,此時instance!=null,但是對象還沒有初始化完成!

instance(memory);    //2.初始化對象

d.由于步驟2和步驟3不存在資料依賴關系,而且無論重排前還是重排後程式的執行結果在單線程中并沒有改變,是以這種重排優化是允許的。但是指令重排隻會保證串行語義的執行的一緻性(單線程),但并不會關心多線程間的語義一緻性。是以當一條線程通路instance不為null時,由于instance執行個體未必已初始化完成,也就造成了線程安全問題。那麼該如何解決呢,很簡單,我們使用volatile禁止instance變量被執行指令重排優化即可。

//禁止指令重排優化

private volatile static DoubleCheckLock instance;