溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Java中Volatile關鍵字怎么使用

發(fā)布時間:2021-07-23 15:39:08 來源:億速云 閱讀:177 作者:Leah 欄目:云計算

這篇文章將為大家詳細講解有關 Java中Volatile關鍵字怎么使用,文章內容質量較高,因此小編分享給大家做個參考,希望大家閱讀完這篇文章后對相關知識有一定的了解。

Volatile 可見性承諾

Java volatile關鍵字保證了跨線程更改線程間共享變量的可見性。這可能聽起來有點抽象,讓我們詳細說明一下。

在多線程應用程序中,線程對 non-volatile 變量進行操作,出于性能原因,每個線程在處理變量時,可以將它們從主內存復制到CPU緩存中。如果你的計算機包含一個以上的CPU,每個線程可以在不同的CPU上運行。這意味著,每個線程可以將同一個變量復制到不同CPU的CPU緩存中。這就和計算機的組成和工作原理息息相關了,之所以在每一個 CPU 中都含有緩存模塊是因為出于性能考慮。因為 CPU 的執(zhí)行速度要比內存(這里的內存指的是 Main Memory)快很多,因為 CPU 要對數(shù)據(jù)進行讀、寫的操作,如果每次都和內存進行交互那么 CPU 在等待 I/O 這個過程中就消耗了大量時間,大部分時間都是在停滯等待而沒有真正投入工作當中。所以為了解決這個問題就引入了CPU緩存。如下圖所示:

Java中Volatile關鍵字怎么使用

這樣就導致了一個問題同一個變量會被不同的 CPU 放在自己的緩存中,對該變量的讀、寫操作在緩存中進行。當然對于非共享數(shù)據(jù)來說這一點問題也沒有,就比如函數(shù)內部的變量,但是對于共享數(shù)據(jù)來說就會造成多個 CPU 之間對該數(shù)據(jù)進行了操作但是別的 CPU 不知道這個數(shù)據(jù)發(fā)生了改變 ,依然使用舊的數(shù)據(jù),最終導致程序不符合我們的預期。因為 CPU 是不知道你的程序內哪些數(shù)據(jù)是多線程共享數(shù)據(jù),而那些數(shù)據(jù)不是,如果你不告訴 CPU 那么它默認都會認為這些數(shù)據(jù)都是不共享的,而各自在自己的緩存中隨意操作。比如這個代碼:

public class VolatileCase0 {
    public int counter = 0;
}

這個代碼在多線程執(zhí)行的環(huán)境下是不安全的,counter 是共享變量。假設兩個 CPU 共同操作同一個 VolatileCase0 對象,如下圖所示:

Java中Volatile關鍵字怎么使用Java中Volatile關鍵字怎么使用

目前這個情況下 counter 在兩個 CPU 緩存中都存在,但是每個 CPU 對 counter 的操作對其他 CPU 來說是不可見的。因為此時我們并沒有告知 CPU 和 CPU 緩存這個 counter 是一個共享內存變量。要解決多個 CPU 緩存之間變量寫操作可見性的問題,就需要用 volatile 關鍵字來修飾這個 counter 。代碼如下:

public class VolatileCase0 {
    public volatile int counter = 0;
}

接下來看一個例子程序:

public class VolatileCase1 {

    volatile boolean running = true;

    public void run() {
        while (running) {

        }
        System.out.println(Thread.currentThread().getName() + " end of execution ");
    }


    public void stop() {
        running = false;
        System.out.println(Thread.currentThread().getName() + " thread Modified running to false");
    }

    public static void main(String[] args) throws Exception {

        VolatileCase1 vc = new VolatileCase1();

        Thread t1 = new Thread(vc::run , "Running-Thread");

        Thread t2 = new Thread(vc::stop , "Stop-Thread");

        t1.start();
        TimeUnit.SECONDS.sleep(1);
        t2.start();

    }

}

如果對 running 變量不加 volatile 關鍵字,程序就會陷在 “Running-Thread”中一直執(zhí)行而無法結束。加上了 volatile 關鍵字之后 “Running-Thread”會讀取到被修改后的 running 值,這時就可以執(zhí)行結束了。

Volatile 禁止指令重排序

首先需要解釋一下什么是“指令重排序”。所謂指令重排序也就是 CPU 對程序指令進行執(zhí)行的時候,會按照自己制定的順序,并不是完全嚴格按照程序代碼編寫的順序執(zhí)行。這樣做的原因也是出于性能因素考慮,CPU對一些可以執(zhí)行的指令先執(zhí)行可以提供總體的運行效率,而不是讓CPU把時間都浪費在停滯等待上面。感興趣的讀者可以參考這篇文章:

感興趣的讀者也可以閱讀 64-ia-32-architectures-software-developer-vol-3a-part-1-manual 這個開發(fā)手冊。以下是該手冊中對于指令重排序的一些描述:

Java中Volatile關鍵字怎么使用

譯文:術語Memory Ordering 是指處理器通過系統(tǒng)總線向系統(tǒng)內存發(fā)出讀(裝入)和寫(存儲)的順序。Intel 64和IA-32體系結構支持多種內存排序模型,具體取決于體系結構的實現(xiàn)。例如,Intel386處理器強制執(zhí)行程序排序(通常稱為強排序),在任何情況下,讀寫都是按指令流中發(fā)生的順序在系統(tǒng)總線上發(fā)出的。

為了優(yōu)化指令執(zhí)行的性能,IA-32體系結構允許在Pentium 4、Intel Xeon和P6系列處理器中偏離稱為處理器排序的強排序模型。這些處理器排序變體(在這里稱為內存排序模型)允許性能增強操作,比如允許讀優(yōu)先于緩沖寫。這些變化的目的是提高指令執(zhí)行速度,同時保持內存一致性,即使在多處理器系統(tǒng)中也是如此。我們通過一個代碼來證實CPU對指令的重排序:

public class MemoryOrderingCase1 {

    static int x = 0 , y = 0 , a = 0 , b = 0;


    public static void main(String[] args) throws Exception {

        while (true) {

            CountDownLatch latch = new CountDownLatch(2);
            x = 0;
            y = 0;
            a = 0;
            b = 0;

            Thread t1 = new Thread(() -> {
                a = 1;
                x = b;
                latch.countDown();
            });


            Thread t2 = new Thread(() -> {
                b = 1;
                y = a;
                latch.countDown();
            });

            t1.start();
            t2.start();

            latch.await();

            if (x == 0 && y == 0) {
                System.out.println("x = " + x + " , y = " + y + " , a = " + a + " , b = " + b);
                break;
            }
        }
    }

}

當 x = 0 同時 y = 0 的時候說明CPU在寫指令完成之前執(zhí)行了讀指令。

另一個例子 Java Double checking locking 單例模式,代碼如下:

public class MemoryOrderingCase2 {

    private static volatile MemoryOrderingCase2 INSTANCE;

    int a;
    int b;

    private MemoryOrderingCase2() {
        a = 1;
        b = 2;
    }

    public static MemoryOrderingCase2 getInstance() {
        if (MemoryOrderingCase2.INSTANCE == null) {
            synchronized (MemoryOrderingCase2.class) {
                if (MemoryOrderingCase2.INSTANCE == null) {
                    MemoryOrderingCase2.INSTANCE = new MemoryOrderingCase2();
                }
            }
        }
        return MemoryOrderingCase2.INSTANCE;
    }
}

在這個例子中如果 INSTANCE 取除掉 volidate 關鍵字就會導致問題的發(fā)生。假設有兩個線程在訪問 getInstance() 函數(shù),執(zhí)行序列如下:

1. 線程 1 進入 getInstance 函數(shù) , INSTANCE 為 null ,并切當前沒有線程持有鎖定。

2. 線程 1 再次判斷 INSTANCE 是否為 null ,結果為 true 。

3. 線程 1 執(zhí)行 INSTANCE = new MemoryOrderingCase2() 。

4. 線程 1 執(zhí)行 new MemoryOrderingCase2() 。

5. 線程 1 在堆內存中為對象分配了空間。

6. 線程 1 INSTANCE 指向了該對象,此時 INSTANCE 已經不為 null。

7. 線程 1 new MemoryOrderingCase2() 對象開始執(zhí)行初始化過程,調用父類構造函數(shù),給一些屬性賦值等。

8. 線程 2 進入 getInstance 函數(shù) ,判斷 INSTANCE 不為 null ,將 INSTANCE 返回。

這里的問題在于 MemoryOrderingCase2 對象還沒有完成全部的初始化過程,就被線程2暴漏給了外界。也就是說讀操作在寫操作還沒有完成之前就發(fā)生了。

查看 getInstance() 函數(shù)的部分匯編代碼:

0x0000000003a663f4: movabs $0x7c0060828,%rdx  ;   {metadata('org/blackhat/concurrent/date20200312/MemoryOrderingCase2')}
  0x0000000003a663fe: mov    0x60(%r15),%rax
  0x0000000003a66402: lea    0x18(%rax),%rdi
  0x0000000003a66406: cmp    0x70(%r15),%rdi
  0x0000000003a6640a: ja     0x0000000003a66557
  0x0000000003a66410: mov    %rdi,0x60(%r15)
  0x0000000003a66414: mov    0xa8(%rdx),%rcx
  0x0000000003a6641b: mov    %rcx,(%rax)
  0x0000000003a6641e: mov    %rdx,%rcx
  0x0000000003a66421: shr    $0x3,%rcx
  0x0000000003a66425: mov    %ecx,0x8(%rax)
  0x0000000003a66428: xor    %rcx,%rcx
  0x0000000003a6642b: mov    %ecx,0xc(%rax)
  0x0000000003a6642e: xor    %rcx,%rcx
  0x0000000003a66431: mov    %rcx,0x10(%rax)    ;*new  ; - org.blackhat.concurrent.date20200312.MemoryOrderingCase2::getInstance@17 (line 24)

  0x0000000003a66435: movl   $0x1,0xc(%rax)     ;*putfield a
                                                ; - org.blackhat.concurrent.date20200312.MemoryOrderingCase2::<init>@6 (line 16)
                                                ; - org.blackhat.concurrent.date20200312.MemoryOrderingCase2::getInstance@21 (line 24)

  0x0000000003a6643c: movl   $0x2,0x10(%rax)    ;*putfield b
                                                ; - org.blackhat.concurrent.date20200312.MemoryOrderingCase2::<init>@11 (line 17)
                                                ; - org.blackhat.concurrent.date20200312.MemoryOrderingCase2::getInstance@21 (line 24)

  0x0000000003a66443: movabs $0x76b907160,%rsi  ;   {oop(a 'java/lang/Class' = 'org/blackhat/concurrent/date20200312/MemoryOrderingCase2')}
  0x0000000003a6644d: mov    %rax,%r10
  0x0000000003a66450: shr    $0x3,%r10
  0x0000000003a66454: mov    %r10d,0x68(%rsi)
  0x0000000003a66458: shr    $0x9,%rsi
  0x0000000003a6645c: movabs $0xf6fd000,%rax
  0x0000000003a66466: movb   $0x0,(%rsi,%rax,1)
  0x0000000003a6646a: lock addl $0x0,(%rsp)     ;*putstatic INSTANCE
                                                ; - org.blackhat.concurrent.date20200312.MemoryOrderingCase2::getInstance@24 (line 24)

關于 Java中Volatile關鍵字怎么使用就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經查實,將立刻刪除涉嫌侵權內容。

AI