java內存模型-內存間交互操作

前言

本文是閱讀周志明大佬的《深入理解Java虛擬機:JVM高級特性與最佳實踐(第3版)》第12章,12.3節Java內存模型得來的讀書筆記。

閱讀告警😂😂😂,本文可能會有點枯燥,大部分內容都是對書中內容做一記錄。示例代碼可能會有不同。

一、內存間交互操作

關於主內存與工作內存之間的具體交互協議,即一個變量如何從主內存拷貝到工作內存、如何從工作內存同步回主內存這一類的實現細節,Java內存模型中定義了8中操作,每一種操作都是原子的、不可再分的

  • lock(鎖定):作用於主內存變量,把一個變量標識爲一條線程獨佔的狀態
  • unlock(解鎖):作用於主內存變量,把一個處於鎖定狀態的變量釋放,釋放後的變量纔可以被其他線程訪問
  • read(讀取):作用於主內存的變量,把一個變量的值從主內存傳輸到線程的工作內存中,以便load操作使用
  • load(載入):作用於工作內存的變量,把read操作從主內存中得到的變量值放入工作內存的變量副本中
  • use(使用):作用於工作內存的變量,它把工作內存中一個變量的值傳遞給執行引擎,每當虛擬機遇到一個需要使用變量的值的字節碼指令時將會執行這個操作
  • assign(賦值):作用於工作內存的變量,它把一個從執行引擎接收的值賦給工作內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操作
  • store(存儲):作用於工作內存的變量,它把工作內存中一個變量的值傳送到主內存中,以便隨後的write操作使用
  • write(寫入):作用於主內存的變量,它把store操作從工作內存中得到的變量的值放入主內存的變量中

如圖所示,變量從主內存到工作內存,需要按順序執行read和load,變量從工作內存回到主內存,按照store和write操作。java內存模型只要求上述兩個操作必須按順序執行,但不要求是連續執行。

java內存模型還規定了執行上述8中基本操作時需要滿足以下規則:

  • 不允許read和load,store和write操作之一單獨出現,即不允許一個變量從主內存讀取了但工作內存不接受,或者工作內存發起回寫了但主內存不接受的情況。
  • 不允許一個線程丟棄它最近的assign操作,即變量在工作內存中改變了之後必須把該變化同步回主內存
  • 不允許一個線程無原因的(沒有發生過任何assign操作)把數據從線程的工作內存同步回主內存中。
  • 一個新的變量只能在主內存中“誕生”,不允許在工作內存中直接使用一個未被初始化(load或assign)的變量,換句話說就是對一個變量實施user、store操作之前,必須先執行assign和load操作
  • 一個變量在同一個時刻只允許一條線程對其進行lock操作,但lock操作可以被同一條線程重複執行多次,多次執行lock後,只有執行相同次數的unlock操作,變量纔會被解鎖
  • 如果對一個變量執行lock操作,那將會清空工作內存中此變量的值,在執行引擎使用這個變量前,需要重新執行load或assign操作以初始化變量的值
  • 如果一個變量沒有被執行lock操作,那麼不允許對它執行unlock操作,也不允許unlock一個被其他線程鎖定的變量
  • 對一個變量執行unlock操作之前,必須先把此變量同步回主內存中
二、volatile變量的特殊規則
2.1 volatile變量的可見性

當變量被定義爲volatile之後, 保證此變量對所有線程的可見性。這裏的“可見性”是指當一條線程修改了這個變量的值,新值對於其他線程來說是可以立即得知的。普通變量並不能做到這一點,因爲普通變量的值在線程間傳遞時均需要通過主內存來完成。

那麼是否可以理解爲volatile變量在各個線程中是一致的,所以基於volatile變量的運算在併發下都是線程安全的???

首先基於我們的經驗,上述話的前半部分是對的,但是結論是不對的。爲什麼呢?我們可以先看一段例子:

public class VolatileTest {
    public static volatile int race = 0;

    public static void increase() {
        race++;
    }
    private static final int THREADS_COUNT = 20;

    public static void main(String[] args) {
        CountDownLatch countDownLatch = new CountDownLatch(THREADS_COUNT);
        ExecutorService service = new ThreadPoolExecutor(5, 8,
                1000, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(1024), new ThreadPoolExecutor.AbortPolicy());
        for (int i = 0; i < THREADS_COUNT; i++) {
            service.execute(() -> {
                for (int i1 = 0; i1 < 100; i1++) {
                    increase();
                }
                countDownLatch.countDown();
            });
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        service.shutdown();
        System.out.println(race);
    }
}

這段代碼發起了20個線程,每個線程對變量進行100次自增操作,如果代碼能正確併發的話,最後輸出的結果是2000。但是我們運行後,並不會獲得期望的結果,而且每次運行的程序輸出的結構都不一樣,是一個小於或者等於2000的數字,這是爲什麼呢?

問題在於race++操作,我們使用javap -v反編譯這段代碼後,發現race++是由4條字節碼構成,如下圖所示

這樣,從字節碼層面就比較容易分析出併發失敗的原因了:當getstatic指令把race讀取到棧頂操作時,volatile關鍵字保證了race的值在此時是正確的,但是在執行iconst_1、iadd這些指令時,其他線程可能已經把race的值改變了,而操作棧頂的值就變成了過期的數據,所以putstatic指令執行後就可能把較小的race同步回主內存中。

2.2 禁止指令重排序優化

普通變量僅會保證在該方法的執行過程中所有依賴賦值結果的地方都能得到正確的結果,而不能保證變量賦值操作的順序與程序代碼中的執行順序一致。

Java內存模型中對volatile變量定義的特殊規則如下:

  • 線程對變量的load、read操作需要連續並且一起出現。即要求在工作內存中,每次使用變量時都必須先從主內存刷新最新的值,用於保證能看見其他線程對變量所做的修改。
  • 線程對變量的store、write操作需要連續並且一起出現。即要求在工作內存中,每次修改變量後都必須立刻同步回主內存中,用於保證其他線程可以看到自己對變量V所做的修改。
  • 假定動作A是線程T對變量V實施的use或assign動作,假定動作F是和動作A相關聯的load或store動作,假定動作P是和動作F相應的對變量V的read或write動作;與此類似,假定動作B是線程T對變量W實施的use或assign動作,假定動作G是和動作B相關聯的load或store動作,假定動作Q是和動作G相應的對變量W的read或write動作。如果A先於B,那麼P先於Q。(這條規則要求volatile修飾的變量不會被指令重排序優化,從而保證代碼的執行順序與程序的順序相同。)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章