java併發程式設計一一多執行緒執行緒安全(三)
1.多執行緒的三大特性
1.1什麼是原子性
即一個操作或多個操作要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行。
一個很經典的例子就是銀行賬戶轉賬問題:
比如從賬戶A向賬戶B轉1000元,那麼必然包括2個操作:從賬戶A減去1000元,往賬戶B加上1000元。這2個操作必須要具備原子性才能保證不出現一些意外的問題。
我們操作資料也是如此,比如i = i+1;其中就包括,讀取i的值,計算i,寫入i。這行程式碼在Java中是不具備原子性的,則多執行緒執行肯定會出問題,所以也需要我們使用同步和lock這些東西來確保這個特性了。
原子性其實就是保證資料一致、執行緒安全一部分,
1.2什麼是可見性
當多個執行緒訪問一個變數時,一個執行緒修改了這個變數的值,其它執行緒能夠立即看到得到這個修改的值。
若兩個執行緒在不同的cpu,那麼執行緒1 改表 i 的值還沒有重新整理到主存,執行緒2又使用 i 的值,
那麼這個i 的值肯定還是之前的,執行緒1 變數的修改沒有看到就是可見性問題。
1.3什麼是有序性
程式的執行順序按照程式碼的先後順序執行。
一般來說處理器為了提高程式的執行效率,可能就會對輸入的程式碼進行優化,他不保證程式中各個語句
的執行先後順序同程式碼中的順序一致,但是他會保證程式最終執行結果和程式碼順序執行的
結果一致的。
示例如下:
int a = 10; //語句1
int r = 2; // 語句2
a = a + 3; // 語句3
r = a* a; // 語句4
則因為重排序,它還可能執行的順序是:2-1-3-4,1-3-2-4
但是絕對不可能是 2-1-4-3 ,因為這個打破了依賴關係。
顯然重排序對單執行緒執行是不會有任何問題的,但是對於多執行緒就不一定了,
所以我們在多執行緒程式設計時就得考慮這個問題。
2.Java記憶體模型
2.1什麼是java記憶體模型
共享記憶體模型指的就是 java 記憶體模型(簡稱 JMM)jmm決定了一個執行緒對共享變數的寫入時,
能對另一個執行緒可見。從抽象的角度來看,jmm 定義了執行緒和主存之間的抽象關係:執行緒之間的
共享變數儲存在主存(main memory)中,每個執行緒都有一個私有的本地記憶體(local memory),
本地記憶體中儲存了該執行緒已讀/寫共享變數的副本。本地記憶體是jmm的一個抽象概念,並不真實存在。
它涵蓋了快取,寫快取區,暫存器以及其他的硬體和編譯器優化。
上圖分析:
執行緒A 與執行緒B 之間如要通訊的話,必須要經歷下面2個步驟:
1. 首先,執行緒A把本地記憶體A 中更新過的共享變數重新整理到主記憶體中。
2. 然後,執行緒B到主記憶體中去讀執行緒A之前已更新過的共享變數
下面通過示意圖來說明這兩個步驟:
如上圖所以:本地記憶體A和B有主記憶體中共享變數x的副本。假設初始時,這三個記憶體中的x值都為0。
執行緒A在執行時,把更新後的x值(假設值為1)臨時存放在自己的本地記憶體中A 中。當執行緒A 和執行緒B
需要通訊時,執行緒A 首先會把自己本地記憶體中修改的x刷到主記憶體中,此時主記憶體中x值變了1.隨後,
執行緒B 到主記憶體中去讀執行緒A更新後的X值,此時執行緒B的本地記憶體的x值也變了1.
從整體來看,這兩個步驟實質是執行緒A 在向執行緒B 發訊息,而且這個通訊過程必須要經過主記憶體。
JMM通過控制主記憶體與每個執行緒的本地記憶體之間的互動,來為java程式設計師提供記憶體可見性保證。
總結:
什麼是java記憶體模型?
java 記憶體模型 簡稱 jmm ,定義一個執行緒對另一個執行緒可見。共享變數存放在主記憶體中,每個執行緒
都有自己的本地記憶體,當多個執行緒同時訪問一個數據的時候,可能本地記憶體沒有及時重新整理到主存中,
所有就會發生執行緒安全問題。
3.Volatile
3.1什麼是Volatile
可見性也就是一旦某個執行緒修改了該被volatile修飾的變數,他也保證修改的值會立即被更新到主記憶體,
當有其它執行緒需要讀取時,可以立即獲取修改之後的值
在java中為了加快程式的執行效率,對一些變數的操作通常是在該執行緒的暫存器或是CPU快取
上進行的,之後才會同步到主記憶體中,而加了volatile修飾符的變數則是直接讀寫到竹村中。
Volatile保證了執行緒間共享變數的及時可見性,但不能保證原子性。
程式碼示例:
class ThreadVolatileDemo extends Thread {
public boolean flag = true;
@Override
public void run() {
System.out.println("開始執行子執行緒....");
while (flag) {
}
System.out.println("執行緒停止");
}
public void setRuning(boolean flag) {
this.flag = flag;
}
}
public class ThreadVolatile {
public static void main(String[] args) throws InterruptedException {
ThreadVolatileDemo threadVolatileDemo = new ThreadVolatileDemo();
threadVolatileDemo.start();
Thread.sleep(3000);
threadVolatileDemo.setRuning(false);
System.out.println("flag 已經設定成false");
Thread.sleep(1000);
System.out.println(threadVolatileDemo.flag);
}
}
已經將結構設定為false為什麼還一直執行呢?
原因:執行緒之間是不可見的,讀取的是副本,沒有及時讀取到主記憶體的結構。
解決方法是使用volatile關鍵字將解決執行緒之間可以見性,強制執行緒每次讀取該值的時候
都去“主記憶體”中取值。
3.2Volatile的特性
保證此變數對所有的執行緒的可見性,這裡的“可見性”。
如文字開頭所述,當一個執行緒修改了這個變數的值,volatile保證了新值能立即同步到主存中,
以及每次使用前立即從主存中重新整理。但是普通的變數做不到這點,普通變數的值線上程間傳遞均需要通過主存來完成。禁止指令重排序優化。
有volatile修飾的變數,賦值後多執行一個“load addl $0x0,(%esp)” 操作,這個操作相當於一個
記憶體屏障(指令重排序時不能把後面的指令重排序到記憶體屏障之前的位置),只有一個CPU
訪問記憶體時,並不需要記憶體屏障;(什麼是指令重排序:是指CPU採用了允許建多條指令不安程式的
的順序分開發送給相應電路單元處理。)volatile 的讀效能消耗與普通變數幾乎相同,但是寫操作稍慢,因為他需要在原生代碼中
插入許多記憶體屏障指令來保證處理器不發生亂序執行。
3.3Volatile與synchronized的區別
- 從而我們可以看出volatile 雖然具有可見性但是並不能保證原子性。
- 效能方面,synchronized關鍵字時防止多個執行緒同時之sing一段程式碼,就會影響程式執行效率,
而volatile 關鍵字在某些情況下效能要優於synchronized。
但是要注意的volatile 關鍵字時無法替代synchrond 關鍵字的,因為volatile關鍵字無法保證原子性。