1. 程式人生 > 其它 >可見性、原子性和有序性問題:併發程式設計Bug的源頭

可見性、原子性和有序性問題:併發程式設計Bug的源頭

1)如何快速而又精準地解決“併發”類的疑難雜症?

  • 理解這件事情的本質,追本溯源,深入分析這些 Bug 的源頭在哪裡。

2)CPU、記憶體、I/O 裝置的速度差異有多大?

  • CPU 是天上一天,記憶體是地上一年(假設 CPU 執行一條普通指令需要一天,那麼 CPU 讀寫記憶體得等待一年的時間)

  • 記憶體和 I/O 裝置的速度差異就更大了,記憶體是天上一天,I/O 裝置是地上十年。

3)為了平衡這三者的速度差異,設計者們做了哪些努力?

  • CPU 增加了快取,以均衡與記憶體的速度差異;

  • 作業系統增加了程序、執行緒,以分時複用 CPU,進而均衡 CPU 與 I/O 裝置的速度差異;

  • 編譯程式優化指令執行次序,使得快取能夠得到更加合理地利用。

4)我們的程式都在享受著前輩們帶來的速度上的優化,但是天下沒有免費的午餐,縮小貧富差距會帶來哪些問題?

  • 快取導致的可見性問題

  • 執行緒切換帶來的原子性問題

  • 編譯優化帶來的有序性問題

5)什麼是可見性?

  • 一個執行緒對共享變數的修改,另外一個執行緒能夠立刻看到

6)為什麼快取會帶來可見性的問題?

  • 以前窮的時候,就一個CPU,一塊快取,所有的執行緒都是他來執行的,都共享他裡面的資料,資料和記憶體裡面的是一致的。

  • 現在日子好過了,有多個CPU,每個CPU有自己的快取。多個執行緒是在不同的CPU上面執行的,操作的是不同的CPU快取資料。如果執行緒A操作完CPU1的快取資料,但是沒有及時向記憶體更新,那我執行緒B是看不見的。

7)思考一下,下面兩個執行緒執行完之後,count值為多少?

public class Test {
private long count = 0;
private void add10K() {
int idx = 0;
while(idx++ < 10000) {
count += 1;
}
}
public static long calc() {
final Test test = new Test();
// 建立兩個執行緒,執行add()操作
Thread th1 = new Thread(()->{
test.add10K();
});
Thread th2 = new Thread(()->{
test.add10K();
});
// 啟動兩個執行緒
th1.start();
th2.start();
// 等待兩個執行緒執行結束,插的是main執行緒的隊
th1.join();
th2.join();
return count;
}
}

8)什麼是原子性?

  • 一個或者多個操作在 CPU 執行的過程中不被中斷的特性稱為原子性

9)什麼是原子性問題?

  • 你看著一條語句執行是不可分的,是整體。但是在CPU眼中,卻是很多步才能執行完。

10)count+=1這條高階語言指令我們的CPU需要幾步才能完成?

  • setp1: 把count變數從記憶體載入到CPU的暫存器

  • stpe2: 在暫存器中執行+1操作

  • step3: 把結果寫入記憶體(快取機制導致可能寫入的是CPU快取而非記憶體)

11)為什麼說我們的CPU分時帶來了原子性的問題?

  • 假如現在有兩個執行緒A和B,都執行上面的語句。A執行完step1之後讓出時間片,B去執行了,B執行完之後又換到A來執行後面的2,3步驟。那結果和我們的預期結果2不相符。

12)什麼是有序性?

  • 程式按照程式碼的先後順序執行

13)什麼是有序性問題?

  • 編譯器為了優化效能,有時候會改變程式中語句的先後順序,例如程式中:“a=6;b=7;”編譯器優化後可能變成“b=7;a=6;”

14)因為編譯器悄悄改變我們程式執行順序而出bug的有哪些案例?

  • 單例模式雙重檢測的時候編譯器改變了我們物件初始化的順序導致出現空指標異常。

15)為什麼long型變數在32位機器上進行加減操作存在併發隱患?

  • Long佔8個位元組,64位,64位的到32位機器上就得拆分命令了,容易出現原子性的問題。