天天看點

JAVA之long和double讀寫操作原子性

Java虛拟機規範定義的許多規則中的一條:所有對基本類型的操作除了某些對long類型和double類型的操作之外,都是原子級的;

當線程把主存中的 long/double類型的值讀到線程記憶體中時,可能是兩次32位值的寫操作,顯而易見,如果幾個線程同時操作,那麼就可能會出現高低2個32位值出錯的情況發生。即long,double高低位問題,非線程安全

舉例說明:

即如有一個long類型的field字段,某個線程正在執行 field = 123L ,而同時有另一個線程正在執行 field = 456L,這樣的指定操作之後field的值會是什麼,是無法保證的。也許是123L,也可能是456L,或許是0L,甚至還可能是31415926L

JVM記憶體模型中定義了8中原子操作

  1. lock:将一個變量辨別為被一個線程獨占狀态
  2. unclock:将一個變量從獨占狀态釋放出來,釋放後的變量才可以被其他線程鎖定
  3. read:将一個變量的值從主記憶體傳輸到工作記憶體中,以便随後的load操作
  4. load:把read操作從主記憶體中得到的變量值放入工作記憶體的變量的副本中
  5. use:把工作記憶體中的一個變量的值傳給執行引擎,每當虛拟機遇到一個使用到變量的指令時都會使用該指令
  6. assign:把一個從執行引擎接收到的值賦給工作記憶體中的變量,每當虛拟機遇到一個給變量指派的指令時,都要使用該操作
  7. store:把工作記憶體中的一個變量的值傳遞給主記憶體,以便随後的write操作
  8. write:把store操作從工作記憶體中得到的變量的值寫到主記憶體中的變量

對于32位作業系統來說單次次操作能處理的最長長度為32bit,而long類型8位元組64bit,是以對long的讀寫都要兩條指令才能完成(即每次讀寫64bit中的32bit);如果JVM要保證long和double讀寫的原子性,勢必要做額外的處理

public class LongAtomTest implements Runnable {

    private static long field = ;

    private volatile long value;

    public long getValue() {
        return value;
    }

    public void setValue(long value) {
        this.value = value;
    }

    public LongAtomTest(long value) {
        this.setValue(value);
    }

    @Override
    public void run() {
        int i = ;
        while (i < ) {
            LongAtomTest.field = this.getValue();
            i++;
            long temp = LongAtomTest.field;
            if (temp != L && temp != -L) {
                System.out.println("出現錯誤結果" + temp);
                System.exit();
            }
        }
        System.out.println("運作正确");
    }

    public static void main(String[] args) throws InterruptedException {
        // 擷取并列印目前JVM是32位還是64位的
        String arch = System.getProperty("sun.arch.data.model");
        System.out.println(arch+"-bit");
        LongAtomTest t1 = new LongAtomTest();
        LongAtomTest t2 = new LongAtomTest(-);
        Thread T1 = new Thread(t1);
        Thread T2 = new Thread(t2);
        T1.start();
        T2.start();
        T1.join();
        T2.join();
    }

}
           

以上代碼在32位JVM上和64位JVM上運作結果将不一緻

從程式得到的結果來看,32位的HotSpot沒有把long和double的讀寫實作為原子操作。 在讀寫的時候,分成兩次操作,每次讀寫32位。因為采用了這種政策,是以64位的long和double的讀與寫都不是原子操作

結論

  • 對于64位的long和double,如果沒有被volatile修飾,那麼對其操作可以不是原子的。在操作的時候,可以分成兩步,每次對32位操作;
  • 如果使用volatile修飾long和double,那麼其讀寫都是原子操作;
  • 在實作JVM時,可以自由選擇是否把讀寫long和double作為原子操作;
  • java中對于long和double類型的寫操作不是原子操作,而是分成了兩個32位的寫操作。讀操作是否也分成了兩個32位的讀呢?在JSR-133之前的規範中,讀也是分成了兩個32位的讀,但是從JSR-133規範開始,即JDK5開始,讀操作也都具有原子性;
  • java中對于其他類型的讀寫操作都是原子操作(除long和double類型以外);
  • 對于引用類型的讀寫操作都是原子操作,無論引用類型的實際類型是32位的值還是64位的值;
  • 對于long類型的不恰當操作可能讀取到從未出現過的值。而對于int的不恰當操作則可能讀取到舊的值;