Volatile

Volatile

標籤(空格分隔): 進程/線程 操作系統


Java虛擬機提供的輕量級的同步機制

1. 保證可見性

不同的線程進入共享內存中讀取數據之後, 在各自的工作空間對數據一通操作, 然後寫入共享內存中, 這個時候因爲共享內存的數據改變, 這個時候會通知其他讀取該共享變量的線程, 通知該數據已經改變.

/**
 * 1. 驗證Volatile的可見性.
 * 1.1 加入number=0; number沒有添加Volatile關鍵字修飾---沒有可見性.
 * 1.2 在該線程對其私有虛擬機棧中棧幀中的備份number操作之後, 不會將數據覆蓋到主內存當中.
 * 2. 加入Volatile之後
 * 2.1 在線程對其備份修改完畢之後, 會將數據覆蓋到主內存當中.
 */
public class VolatileDemo {
    public static void main(String[] args) {
        val myData = new MyData();
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t come in");
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myData.addTo60();
            System.out.println(Thread.currentThread().getName() + "\t updated number value: " + myData.number);
        }, "aaa").start();

        while (myData.number == 0) {

        }

        System.out.println("任務結束, number: " + myData.number);

    }
}
class MyData {
//    volatile  int number = 0;
    int number = 0;

    public void addTo60() {
        this.number = 60;
    }

    public void addPlusPlus() {
        this.number++;
    }
}

2. 不保證原子性

/**
 * Volatile 不保證原子性
 *
 * 在多個線程對棧幀中的number修改完畢之後, 在A線程馬上開始寫入主內存的時候被打斷了, 這個時候B線程把自己的計算結果寫入了
 * 這個時候, 就會產生計算結果被覆蓋的情況. 然後永遠都計算不出來正確的值.  這個就是Volatile的原子性問題.
 */
public class VolatileDemo1 {
    public static void main(String[] args) throws InterruptedException {
        MyData myData = new MyData();


        for (int i = 0; i < 20; i++) {

            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    myData.addPlusPlus();
                }
            }, String.valueOf(i)).start();

        }

        /* main線程   和  後臺GC線程 */
        while(Thread.activeCount()>2){
            Thread.yield();
        }
        System.out.println(myData.number);
        System.out.println("剩餘線程數量"+ Thread.activeCount());
    }
}

3. 禁止指令重排

指令隊列在CPU執行時不是串行的, 當某條指令執行時消耗較多時間時, CPU資源足夠時並不會在此無意義的等待, 而是開啓下一個指令. 開啓下一條指令是有條件的, 即上一條指令和下一條指令不存在相關性. 例如下面這個例子:

a /= 2;   // 指令A
a /= 2;   // 指令B
c++;      // 指令C

這裏的指令B是依賴於指令A的執行結果的, 在A處於執行階段時, B會被阻塞, 直到A執行完成. 而指令C與A/B均沒有依賴關係, 所以在A執行或者B執行的過程中, C會同時被執行, 那麼C有可能在A+B的執行過程中就執行完畢了, 這樣指令隊列的實際執行順序就是 C->A->B 或者 A->C->B.

可能出現指令重排導致問題的代碼

public void method1()  {
    a = 1;                  // 語句1
    flag = true;            // 語句2
}

public void method2() {
    if (flag) {
        a = a + 5;
    }
}

工作區域和主內存出現的同步延遲現象導致的可見性問題可以使用synchronize或volatile解決. 他們都可以使一個線程修改後的變量立即對其他線程可見.

運算指令

運算由運算器單元(ALU)實現,指令包括算術運算指令、邏輯運算指令和移位指令。
算術運算指令實現加減乘除(+-*/)等基本的算術運算;邏輯運算指令實現與或非(&|~)等基本的邏輯運算;移位指令實現二進制比特位(bit)的左右移(<<>>)運算。

控制指令

除了做計算外,CPU還要實現循環。循環是由跳轉指令實現的,跳回去執行就是循環。循環在一定條件下跳出,否則就成死循環了,條件跳轉指令能完成這個功能。條件跳轉指令在一定條件下實現跳轉,它能實現分支功能。跳轉指令也稱爲控制指令。控制由CPU控制器單元實現。

數據傳送指令

運算和控制指令的操作數從哪裏來的呢?操作數都放在存儲器中。在x86 IA中,運算指令的操作數既可以是寄存器,也可以是存儲器;而在其他RISCIA例如MIPS中,運算指令的操作數只能是寄存器,因此需要先使用加載(load)指令將存儲器中的數據導入到寄存器中,運算完成後,再用存儲(store)指令將寄存器中的運算結果數據導出到存儲器中。這類指令就是數據傳送指令。


可見性的實現就是 數據傳送指令中的先使用加載(load)指令將存儲器中的數據導入到寄存器中,運算完成後,再用存儲(store)指令將寄存器中的運算結果數據導出到存儲器中。


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