併發編程以內存可見性java
在上篇線程安全中,咱們已經知道須要使用鎖來同步管理對可變狀態的訪問操做。今天咱們來看下併發編程的內存可見性問題。編程
同步代碼塊除了實現原子性或者臨界區以外,其還保證了內存可見性,即保證其餘線程能夠看到狀態的變化結果。緩存
private static boolean stop =false; private static int number = 0; public static class ReaderThread extends Thread { public void run() { while(!stop) { Thread.yield(); } System.out.println(number); } } public static void main(String[] args) { new ReaderThread().start(); number=42; stop=true; }
在代碼中,主線程和讀線程都將訪問stop和number。主線程啓動讀線程並將number設置爲42、stop設置爲true。讀線程一直循環知道發現stop爲true中止安全
並輸出number的值。因爲代碼沒有足夠的同步機制,以及可能會存在指令重排序,因此最終可能會輸出42,也可能會輸出0,也有可能會一直循環沒法停下來。多線程
注意:併發
在沒有同步的狀況下,編譯器、處理器、運行時等均可能對操做執行順序進行調整,所以沒法對內存的操做順序進行斷定。this
因爲cpu多級緩存的存在,多線程共享狀態的修改涉及到各級緩存、內存之間的同步問題。spa
1、失效數據線程
在上邊的例子中,讀線程可能會讀取stop變量失效的值,從而致使程序無限循環而沒法中止下來。code
失效的數據可能致使引用失效、意外的異常、非法的數據、死循環等問題。
例以下邊的 UnSafeInteger類並非線程安全的,可能致使一個線程set以後,另一個線程get的時候可能得不到最新的值。
package com.codeartist; public class UnSafeInteger { private int value; public int get() { return this.value; } public void set(int value) { this.value = value; } }
咱們能夠經過對set和get使用同一個鎖進行同步,這樣就能夠防止get到失效的值。
package com.codeartist; public class SyncInteger { private int value; public synchronized int get() { return this.value; } public synchronized void set(int value) { this.value = value; } }
2、非原子的64位操做和Volatile
失效的數據最終讀取的仍是以前線程設置的值,因爲變量的讀取和寫入操做都是原子性的,因此絕大多數的變量均可以保證這個最低安全性。
讀取和寫入64位的long和double變量的操做會分解爲兩個32位的操做。在多個線程寫入和讀取時可能致使讀取某個值的高32位和另一個值的低32位。
java和.NET都提供了一種弱的同步機制,即volatile變量,訪問volatile變量不會加鎖致使阻塞。
volatile經過確保不會對共享狀態變量施加的操做進行重排序,也不會將其緩存到cpu不可見的地方,從而保證了共享狀態的內存可見性。
volatile變量值確保可見性,並不能確保操做的原子性,因此其只適合做爲簡單的標誌判斷操做是否完成、中斷等。
只有知足如下條件的時候,才應該考慮使用volatile
1.對該變量的操做不依賴變量的當前值,好比自增、自減都是不適合使用的。
2.該變量不會與其餘狀態變量一塊兒做爲不變性條件。
3.訪問該變量不須要加鎖進行同步。
3、鎖定與可見性
加鎖能夠確保某個線程以一種可預測的方式查看其餘線程的執行結果,即下圖中,當線程A獲取M鎖執行同步代碼塊的時看到的變量x的值,待鎖釋放以後,線程B也能夠看到。
只要保證讀取和寫入操做使用同一個鎖同步,便可以保證操做的互斥性,也能夠保證全部線程都能看到共享變量的最新值的內存可見性。