簡體   English   中英

Java內存模型同步:如何誘導數據可見性bug?

[英]Java memory model synchronization: how to induce data visibility bug?

“Java Concurrency in Practice”給出了以下不安全類的示例,由於Java內存模型的性質可能最終會永久運行或打印0。

這個類試圖證明的問題是這里的變量不是線程之間的“共享”。 因此,線程看到的值可能與另一個線程不同,因為它們不是易失性或同步的。 另外由於JVM ready = true允許的語句的重新排序可能在number = 42之前設置。

對我來說,這個類總是使用JVM 1.6正常工作。 有關如何讓這個類執行不正確的行為(即打印0或永遠運行)的任何想法?

public class NoVisibility {
    private static boolean ready;
    private static int number;

    private static class ReaderThread extends Thread {
        public void run() {
            while (!ready)
                Thread.yield();
            System.out.println(number);
        }
    }

    public static void main(String[] args) {
        new ReaderThread().start();
        number = 42;
        ready = true;
    }
}

java內存模型定義了工作所需的內容和不需要的內容。 不安全的多線程代碼的“美”在大多數情況下(特別是在受控的開發環境中)通常都有效。 只有當你使用更好的計算機進行生產並且負載增加時,JIT才能真正開始嘗試使用錯誤。

您遇到的問題是您沒有等待足夠長的時間來優化代碼並緩存值。

當x86_64系統上的線程第一次讀取值時,它將獲得一個線程安全副本。 它只是后來的變化,它無法看到。 其他CPU可能不是這種情況。

如果您嘗試這樣做,您可以看到每個線程都卡在其本地值。

public class RequiresVolatileMain {
    static volatile boolean value;

    public static void main(String... args) {
        new Thread(new MyRunnable(true), "Sets true").start();
        new Thread(new MyRunnable(false), "Sets false").start();
    }

    private static class MyRunnable implements Runnable {
        private final boolean target;

        private MyRunnable(boolean target) {
            this.target = target;
        }

        @Override
        public void run() {
            int count = 0;
            boolean logged = false;
            while (true) {
                if (value != target) {
                    value = target;
                    count = 0;
                    if (!logged)
                        System.out.println(Thread.currentThread().getName() + ": reset value=" + value);
                } else if (++count % 1000000000 == 0) {
                    System.out.println(Thread.currentThread().getName() + ": value=" + value + " target=" + target);
                    logged = true;
                }
            }
        }
    }
}

打印以下內容顯示其掠過值,但卡住了。

Sets true: reset value=true
Sets false: reset value=false
...
Sets true: reset value=true
Sets false: reset value=false
Sets true: value=false target=true
Sets false: value=true target=false
....
Sets true: value=false target=true
Sets false: value=true target=false

如果我添加-XX:+PrintCompilation此切換會在您看到的時間發生

1705    1 % RequiresVolatileMain$MyRunnable::run @ -2 (129 bytes)   made not entrant
1705    2 % RequiresVolatileMain$MyRunnable::run @ 4 (129 bytes)

這表明代碼已編譯為本機是一種非線程安全的方式。

如果你使價值volatile你會看到它無休止地翻轉價值(或直到我感到無聊)

編輯:這個測試的作用是什么; 當它檢測到的值不是線程目標值時,它會設置該值。 即。 線程0設置為true ,線程1設置為false當兩個線程正確共享字段時,它們會看到彼此的更改,並且值會在true和false之間不斷翻轉。

如果沒有volatile,則會失敗並且每個線程只會看到自己的值,因此它們都會更改值,線程0看到true而線程1看到同一字段的false

不是100%肯定這一點,但是可能與:

重新排序是什么意思?

在許多情況下,對程序變量(對象實例字段,類靜態字段和數組元素)的訪問可能看起來以與程序指定的順序不同的順序執行。 編譯器可以自由地使用優化名稱中的指令順序。 處理器可能在某些情況下不按順序執行指令。 可以以不同於程序指定的順序在寄存器,處理器高速緩存和主存儲器之間移動數據。

例如,如果一個線程寫入字段a然后寫入字段b,並且b的值不依賴於a的值,則編譯器可以自由地重新排序這些操作,並且緩存可以自由地將b刷新到main記憶之前的。 有許多潛在的重新排序源,例如編譯器,JIT和緩存。

編譯器,運行時和硬件應該合謀創建as-if-serial語義的假象,這意味着在單線程程序中,程序不應該能夠觀察重新排序的影響。 但是, 重新排序可能會在錯誤同步的多線程程序中發揮作用,其中一個線程能夠觀察其他線程的影響,並且可能能夠檢測到變量訪問對於其他線程可見,其順序與執行或指定的順序不同。計划

我認為關於這一點的要點是,並不能保證所有的jvms都會以相同的方式重新排序指令。 它用作存在不同可能的重新排序的示例,因此對於jvm的某些實現,您可能會得到不同的結果。 只是碰巧你jvm每次都以相同的方式重新排序,但對另一個人來說情況可能並非如此。 保證排序的唯一方法是使用正確的同步。

根據您的操作系統,Thread.yield()可能會也可能不起作用。 Thread.yield()實際上不能被視為獨立於平台,如果您需要這個假設,則不應該使用它。

讓這個例子做你期望它做的事情,我認為這更像是處理器架構而不是其他任何東西......嘗試在不同的機器上運行它,使用不同的操作系統,看看你可以從中獲得什么。

請參閱下面的代碼,它介紹了x86上的數據可見性錯誤。 試過jdk8和jdk7

package com.snippets;


public class SharedVariable {

    private static int  sharedVariable = 0;// declare as volatile to make it work
    public static void main(String[] args) throws InterruptedException {

        new Thread(new Runnable() {

            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                sharedVariable = 1;
            }
        }).start();

        for(int i=0;i<1000;i++) {
            for(;;) {
                if(sharedVariable == 1) {
                    break;
                }
            }
        }
        System.out.println("Value of SharedVariable : " + sharedVariable);
    }

}

Trick不期望處理器進行重新排序而是讓編譯器進行一些優化,這會引入可見性錯誤。

如果運行上面的代碼,您將看到它無限期掛起,因為它永遠不會看到更新的值sharedVariable。

要更正代碼,請將sharedVariable聲明為volatile。

為什么普通變量不起作用而上述程序掛起?

  1. sharedVariable未聲明為volatile。
  2. 現在因為sharedVariable未被聲明為volatile編譯器優化代碼。 它看到sharedVariable不會被改變,所以為什么我應該每次在循環中讀取內存。 這將使sharedVariable脫離循環。 類似於下面的東西。

F

for(int i=0;i<1000;i++)/**compiler reorders sharedVariable
as it is not declared as volatile
and takes out the if condition out of the loop
which is valid as compiler figures out that it not gonna  
change sharedVariable is not going change **/
    if(sharedVariable != 1) {  
     for(;;) {}  
    }      
}

在github上共享: https//github.com/lazysun/concurrency/blob/master/Concurrency/src/com/snippets/SharedVariable.java

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM