這里我們使用Java的線程與鎖來解析共享內存模型;做過java開發並且了解線程安全問題的知道,要使某段代碼是線程安全的那必須要滿足兩個條件:內存可見性、原子性;
內存可見性
在JVM規定多個線程進行通訊是通過共享變量進行的,而Java內存模型規定了有主內存是所有線程共享的,而各個線程又有自己的工作內存,線程只能訪問自己的工作內存中數據;
如:有一個共享變量x,兩個線程a、b變量x存儲在主內存中然后又兩個x的拷貝分別存儲在a、b線程的工作內存中線程a、b只能對自己工作內存中的x的拷貝進行操作,不可直接操作主內存;
線程a對x修改時先把值放到自己的工作內存中,然后再把工作內存中的x拷貝更新到主內存中,線程b同樣如此;當線程a更新了主內存后線程b刷新工作內存后就能看到a更新后的最新值這就是內存可見性問題;
內存可見性要保證兩點:1、線程修改后的共享變量更新到主內存;2、從主內存中更新最新值到工作內存中;
內存可見性:線程對共享變量的修改其他線程可以看到修改后的值;
原子性
當線程引用共享變量時,工作內存中沒有共享變量時它會從主內存復制共享變量到自己工作內存中,當工作內存有共享變量時線程可能會從主內存更新也有可能直接使用工作內存中的共享變量;
有代碼塊,count為共享變量:
1 ++count;
// count初始值為0,這時有a、b線程都執行這行代碼,可能有不少人以為線程a , b執行完成后count的值為2,但真實情況是count最終值可能為1也可能為2,因為這里有一個原子性問題;
熟悉Java的都知道在Java中++count非原子操作,流程為:
1、把主內存共享變量count拷貝到工作內存
2、把工作內存中count值+1
3、把結果寫回更新回主內存
當只有一個線程時這個操作沒有問題;
當有多個線程時有可能出現:
1、 線程a把主內存共享變量count拷貝到工作內存
2、 線程b把主內存共享變量count拷貝到工作內存
3、 線程a把工作內存中count進行+1
4、 線程b把工作內存中count進行+1
5、 線程a把工作內存更新到主內存
6、 線程b把工作內存更新到主內存
a,b線程執行完后最終count的值只是1而不是我們期望得到的2,因為這里出現了多個線程交叉執行導致破壞了程序的有序性,而且count+1操作又不是原子的,所以我們必須要保證這程序的原子性,可以使用Java中的synchronized(同步)或Lock機制來解決;
使用共享內存模型進行並發編程時必須要解決我們上面介紹的兩個點:內存可見性、原子性,但現在大部分編程語言原生都支持共享內存模型方式的並發所以我們很容易就可以達到這兩個要求;
現在代碼的執行要經過多層的優化對指令重排序,如:編譯器、處理器等級別的優化,經過這些優化重排序后最終代碼執行順序可能與之前是不一致的,在單線程時中編譯器、處理會保持as-if-serial,對不存在數據依賴的進行重排序,所以不會出現重排序問題;但在多線程情況下就會出現問題,不過還好Java中有些機制可以使程序在編譯器、處理器優化時會對有數據依賴的禁止指令重排序,如:volatile、synchronized等,所以我們可以很輕松應對這問題;
指令重排序問題
在Java中我們要使代碼在多線程中同時滿足內存可見性與有序性那就要使用Java提供給我們的同步與鎖機制如:synchronized、volatile、Lock、concurrent類等;
優點:共享內存模型(線程與鎖)可以說是最常見的並發模型大多數編程語言都原生支持,也適合解決很多問題,通過線程與鎖實現起來相對也簡單點;
缺點:通過多線程實現並發,而線程耗費的資源比較多,線程總數有限制;通過共享內存來實現多線程通訊又會涉及到鎖、竟態、死鎖等問題影響程序性能;一不小心就會陷入可見性問題、重排序問題等而且多線程程序不容易測試、維護等;
文章首發地址:Solinx
http://www.solinx.co/archives/179