volatile關鍵字的如何保證內存可見性,爲啥不保證原子性

volatile關鍵字的如何保證內存可見性,爲啥不保證原子性

首先,我們來一段程序演示一下

/**
 * @program: mayun-quick_Netty
 * @description: volatile關鍵字的如何保證內存可見性
 * @author: Mr.Liu
 * @create: 2019-11-17 12:56
 **/
public class volatileTest {
    public static void main(String[] args) {
        ThreadDemo td = new ThreadDemo();
        new Thread(td).start();
        while(true){
            if (td.isTag()){
                System.err.println("================>>"+td.isTag());
                break;
            }
        }
    }

}
class ThreadDemo implements Runnable{
    private boolean tag = false;
    public boolean isTag(){
        return tag;
    }
    public void setTag(boolean tag){
        this.tag = tag;
    }
    @Override
    public void run() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        tag = true;
        System.out.println("tag = "+tag);
    }
}

有兩個線程,一個修改tag的值爲true,主線程獲取tag的值做判斷,結果:

主線程未結束,當前tag的值還是false,所以沒有打印========>>>true的值.
按理通過Runnable創建的線程訪問的應該是共享數據,那爲什麼會出現這種情況?這就涉及到內存可見性。

JVM會爲每個線程分配一個獨立緩存提高效率
在這裏插入圖片描述
那麼這個兩個線程,一個是讀(主線程),一個是寫(線程1),我們讓線程1睡了2s,說明,線程1先執行,每個線程都有一個獨立的緩存,也就是說當線程1需要對主存的共享數值進行改變,它需要先把這個flag複製一份到緩存區中,
在這裏插入圖片描述
然後修改,將來再把這個值寫回主存去,在寫之前,主線程來了,它要讀取現在在內存裏面的值,現在是false,當然有一種情況,就是線程1在某個機會將flag=true寫回去,
在這裏插入圖片描述
在這裏插入圖片描述
當時主線程用了while(true),這句話調用了系統底層代碼,效率極高,高到主線程沒有機會再次讀取內存,這就是線程對共享數據操作的不可見。

內存可見性問題:當多個線程操作共享數據時,彼此不可見。
如何解決?同步鎖。改寫main方法

 ThreadDemo td = new ThreadDemo();
        new Thread(td).start();
        while(true){
            synchronized (td){
                if (td.isTag()){
                    System.err.println("================>>"+td.isTag());
                    break;
                }
            }
        }

在這裏插入圖片描述

但是用了鎖,代表效率極低,但是我現在我不想加鎖,但是有存在內存可見性的問題,我該怎麼辦?

關鍵字volatile:當多個線程進行操作共享操作時,可以保證內存中的數據可見。(內存柵欄,實時刷新)
被volatile關鍵字修飾的變量,在每個寫操作之後,都會加入一條store內存屏障命令,此命令強制工作內存將此變量的最新值保存至主內存;在每個讀操作之前,都會加入一條load內存屏障命令,此命令強制工作內存從主內存中加載此變量的最新值至工作內存。

我們可以認爲它是直接在主存操作的,這個實時刷新的操作相比不加,性能略低,但是比加鎖的效率顯然高很多,低在哪?加了這關鍵字,JVM就不能進行指令重排序,無法優化代碼執行

//修改代碼中這個,添加了volatile關鍵字
private volatile boolean tag = false;

在這裏插入圖片描述
volatile相對synchronized是一種輕量級同步策略。但是注意:

  1. volatile不具備互斥性
  2. volatile不能保證變量的原子性

Java中long和double賦值不是原子操作,因爲先寫32位,再寫後32位,分兩步操作,這樣就線程不安全了。如果改成下面的就線程安全了

private volatile long number = 8;

那麼,爲什麼是這樣?volatile關鍵字難道可以保證原子性?
volatile僅僅用來保證該變量對所有線程的可見性,但不保證原子性。但是我們這裏的例子,volatile似乎是有時候可以代替簡單的鎖,似乎加了volatile關鍵字就省掉了鎖。這不是互相矛盾嗎

其實如果一個變量加了volatile關鍵字,就會告訴編譯器和JVM的內存模型:這個變量是對所有線程共享的、可見的,每次jvm都會讀取最新寫入的值並使其最新值在所有CPU可見。所以說的是線程可見性,沒有提原子性

下面我們用一個例子說明volatile沒有原子性,不要將volatile用在getAndOperate場合(這種場合不原子,需要再加鎖,如i++),僅僅set或者get的場景是適合volatile的。
例如你讓一個volatile的integer自增(i++),其實要分成3步:

  1. 1)讀取volatile變量值到local;
  2. 2)增加變量的值;
  3. 3)把local的值寫回,讓其它的線程可見。

這3步的jvm指令爲:

mov    0xc(%r10),%r8d ; Load
inc    %r8d           ; Increment
mov    %r8d,0xc(%r10) ; Store
lock addl $0x0,(%rsp) ; StoreLoad Barrier

注意最後一步是內存屏障
什麼是內存屏障(Memory Barrier)?
內存屏障(memory barrier)是一個CPU指令。基本上,它是這樣一條指令:

  1. a) 確保一些特定操作執行的順序;
  2. b) 影響一些數據的可見性(可能是某些指令執行後的結果)。

編譯器和CPU可以在保證輸出結果一樣的情況下對指令重排序,使性能得到優化。

插入一個內存屏障,相當於告訴CPU和編譯器先於這個命令的必須先執行,後於這個命令的必須後執行。

內存屏障另一個作用是強制更新一次不同CPU的緩存。

上面的 中間的幾步(從Load到Store)是不安全的,中間如果其他的CPU修改了值將會丟失
所以volatile保證變量對線程的可見性,但不保證原子性

參考:https://www.cnblogs.com/figsprite/p/10779904.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章