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)指令將寄存器中的運算結果數據導出到存儲器中。