Java多線程之內存可見性(sync和volatile都可以)和原子性操作


可見性的理論

就說這個線程是可見的

 

 

工作內存是java內存模型提出的概念

JMM

變量是指共享變量

下面的X就是三個線程的共享變量

 

 

 

 共享變量可見性的原理

兩個步驟其中任何一個步驟出了差錯,都會導致變量不可見。會導致數據的不准確,從而是的線程不安全。所以在編寫代碼的時候要保證共享變量的可見性

 

 


 滿足兩點可以保證可見性

 

這里指語言層面,所以不包括concurrent並發包下的高級特性

可以實現互斥鎖(原子性),用synchronized

但是他也有另個功能,即實現內存的可見性

Synchronized實現可見性

這六個步驟可以結合剛剛的兩條規定來理解。

 

先補充一個概念:

不理解也沒關系

看的懂上面的意思即可,有可能執行順序和代碼順序不一樣

as-if-serial

 

 

在單線程下一定遵循這個條件

 

舉個例子:

 

 

package mkw.demo.syn;

public class SynchronizedDemo {
    //共享變量
    private boolean ready = false;
    private int result = 0;
    private int number = 1;   
    //寫操作
    public void write(){
        ready = true;                           //1.1                
        number = 2;                            //1.2                
    }
    //讀操作
    public void read(){                    
        if(ready){                             //2.1
            result = number*3;         //2.2
        }       
        System.out.println("result的值為:" + result);
    }

    //內部線程類
    private class ReadWriteThread extends Thread {
        //根據構造方法中傳入的flag參數,確定線程執行讀操作還是寫操作
        private boolean flag;
        public ReadWriteThread(boolean flag){
            this.flag = flag;
        }
        @Override                                                                    
        public void run() {
            if(flag){
                //構造方法中傳入true,執行寫操作
                write();
            }else{
                //構造方法中傳入false,執行讀操作
                read();
            }
        }
    }

    public static void main(String[] args)  {
        SynchronizedDemo synDemo = new SynchronizedDemo();
        //啟動線程執行寫操作
        synDemo .new ReadWriteThread(true).start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        //啟動線程執行讀操作
        synDemo.new ReadWriteThread(false).start();
    }
}

 

 

假設在1.1后讓出了cpu資源,那么這時候執行2.1,也就是讀線程開始執行,

結果是3

先執行1.2說明進行了指令重排序,打印了初始值 0

 還可以2.1和2.2重排序,

 

 

 Volatile實現可見性

加入內存屏障和禁止指令重排序實現可見性。

把內容強制刷新到主內存中去

 

 

在java種一共有八條操作指令,store和load是其中的兩條。

通俗理解:

volatile變量在每次被線程訪問的時候,都強迫從主內存中重讀該變量的值,而當該變量發生變化的時候,又會強迫線程將更新的值刷新到主內存。

這樣任何時刻,不同的線程總能看到該變量的最新值。

 

線程寫volatile變量的過程:

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

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

 

線程讀volatile變量的過程:

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

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

 

volatile不能保持原子性

volatile不能保證volatile變量復合操作的原子性

 

number++可以分解成三個步驟【重】,所以是線程不安全的,所以用sync加上鎖后就是原子操作,三個步驟就合成了一個步驟,必須同時執行。

 

但是如果用volatile修飾的話:

 

舉個例子:volatile修飾number

有兩個線程A和B,

首先a搶到cpu,讀取num的值;讀完(讀操作不會將工作內存的值立刻刷新到主內存,只有對num寫才會刷新到主內存中)之后讓出cpu,b搶到cpu,a線程阻塞,然后b讀取,b執行num+1操作,這時候由於volatile的對內存可見性,會把最新值刷新到主內存中去,所以這時候主內存中num的值為6。

可是剛剛a讀取num的值的時候num還是5.這時候如果b讓出cpu,a搶到cpu的 時候不會重新讀num的值,而是直接執行+1操作了。

 

從第五步開始A執行+1操作,num本來主內存已經是6了,現在又從5+1變成6。可見兩個線程分別對num+1,可是看起來,只加了一次。

這是由於volatile不能保持原子性

 禁止指令重排序

https://blog.csdn.net/javazejian/article/details/72772461#理解java內存區域與java內存模型

 

 

volatile禁止重排優化

volatile關鍵字另一個作用就是禁止指令重排優化,從而避免多線程環境下程序出現亂序執行的現象,關於指令重排優化前面已詳細分析過,這里主要簡單說明一下volatile是如何實現禁止指令重排優化的。先了解一個概念,內存屏障(Memory Barrier)。 
內存屏障,又稱內存柵欄,是一個CPU指令,它的作用有兩個,一是保證特定操作的執行順序,二是保證某些變量的內存可見性(利用該特性實現volatile的內存可見性)。由於編譯器和處理器都能執行指令重排優化。如果在指令間插入一條Memory Barrier則會告訴編譯器和CPU,不管什么指令都不能和這條Memory Barrier指令重排序,也就是說通過插入內存屏障禁止在內存屏障前后的指令執行重排序優化。Memory Barrier的另外一個作用是強制刷出各種CPU的緩存數據,因此任何CPU上的線程都能讀取到這些數據的最新版本。總之,volatile變量正是通過內存屏障實現其在內存中的語義,即可見性和禁止重排優化。下面看一個非常典型的禁止重排優化的例子DCL,如下:

/** * Created by zejian on 2017/6/11. * Blog : http://blog.csdn.net/javazejian [原文地址,請尊重原創] */ 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; } }

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

memory = allocate(); //1.分配對象內存空間 instance(memory); //2.初始化對象 instance = memory; //3.設置instance指向剛分配的內存地址,此時instance!=null

由於步驟1和步驟2間可能會重排序,如下:

memory = allocate(); //1.分配對象內存空間 instance = memory; //3.設置instance指向剛分配的內存地址,此時instance!=null,但是對象還沒有初始化完成! instance(memory); //2.初始化對象

由於步驟2和步驟3不存在數據依賴關系,而且無論重排前還是重排后程序的執行結果在單線程中並沒有改變,因此這種重排優化是允許的。但是指令重排只會保證串行語義的執行的一致性(單線程),但並不會關心多線程間的語義一致性。所以當一條線程訪問instance不為null時,由於instance實例未必已初始化完成,也就造成了線程安全問題。那么該如何解決呢,很簡單,我們使用volatile禁止instance變量被執行指令重排優化即可。

  //禁止指令重排優化 private volatile static DoubleCheckLock instance;

ok~,到此相信我們對Java內存模型和volatile應該都有了比較全面的認識,總而言之,我們應該清楚知道,JMM就是一組規則,這組規則意在解決在並發編程可能出現的線程安全問題,並提供了內置解決方案(happen-before原則)及其外部可使用的同步手段(synchronized/volatile等),確保了程序執行在多線程環境中的應有的原子性,可視性及其有序性。

實現原子性的解決方法:

第一種方法:

 

 鎖整個方法:

這個方式,使得效率比較低,因為休眠100毫秒,B線程必須等待完A休眠完才行

鎖代碼塊:

這樣效率更高

第二種方法(可實現可見性 原子性)

1.

 2.

 

3.

 

 volatile適用場景

 

如果有兩個volatile變量,每個volatile變量狀態要獨立於其他volatile變量

感覺volatile沒有sync實用

 sync和volatile的比較

所以在保證volatile適用場景的情況下,實用volatile更加輕量級。

 

注:final也可保證可見性,因為他修飾的變量本身就是不能被改變的。

 

補充一點:

如果想要避免這種情況,就用關鍵字volatile聲明64位的變量,或者把對他們的讀寫操作鎖起來。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM