Java併發——DCL問題

轉自:http://www.iteye.com/topic/875420java

若是你搜索網上分析dcl爲何在java中失效的緣由,都會談到編譯器會作優化云云,我相信你們看到這個必定會以爲很沮喪、很無助,對本身寫的程序很沒信心。我很理解這種感覺,由於我也經歷過,這或許是爲何網上一直有人喜歡談dcl的緣由。若是放在java5以前,從編譯器的角度去解釋dcl也無可厚非,在java5的JMM(內存模型)已經獲得很大的修正,若是到如今還只能從編譯器的角度去解釋dcl,那簡直就在污辱java,要知道java的最大優點就是隻須要考慮一個平臺。你能夠徹底無視網上絕大多數關於dcl的討論,不少時候他們本身都說不清楚,除Doug Lea等幾個大牛,我不相信誰比誰更權威。 

不少人不理解dcl,不是dcl有多麼複雜,偏偏相反,而是對基礎掌握得不夠。因此,我會先從基礎講起,而後再分析DCL。 

咱們都知道,當兩個線程同時讀寫(或同時寫)一個共享變量時會發生數據競爭。那我怎麼才能知道發生了數據競爭呢?我須要去讀取那個變量,發生數據競爭一般有兩個表現:一是讀取到陳舊數據,即讀取到雖是曾經寫入的數據,但不是最新的。二是讀取到以前根本沒有寫入的值,也就是說讀到垃圾。 

數據陳舊性 

爲了讀取到另外一個線程寫入的最新數據,JMM定義了一系列的規則,最基本的規則就是要利用同步。在Java中,同步的手段有synchronized和volatile兩種,這裏我只會涉及到syncrhonized。請你們先記住如下規則,接下來我會細講。 

規則一:必須對變量的全部寫和全部讀同步,才能讀取到該最新的數據。 

先看下面的代碼: 數據庫

public class A {
	private int some;
	public int another;

	public int getSome() { return some; }
	public synchronized int getSomeWithSync() { return some; }
	public void setSome(int v) { some = v; }
	public synchronized void setSomeWithSync(int v) { some = v; }
}

讓咱們來分析一個線程寫,另外一個線程讀的情形,一共四種情形。初始狀況都是a = new A(),暫不考慮其它線程。 

情形一:讀寫都不一樣步。 編程

 

Thread1 Thread2
(1) a.setSome(13)  
  (2) a.getSome()

 

 這種狀況下,即便thread1先寫入some爲13,thread2再讀取some,它能讀到13嗎?在沒有同步協調下,結果是不肯定的。從圖上看出,兩個線程獨立運行,JMM並不保證一個線程可以看到另外一個線程寫入的值。在這個例子中,就是thread2可能讀到0(即some的初始值)而不是13。注意,在理論上,即便thread2在thread1寫入some以後再等上一萬年也仍是可能讀到some的初始值0,儘管這在實際幾乎不可能發生。 多線程

 情形二:寫同步,讀不一樣步 併發

Thread1 Thread2
(1) a.setSomeWithSync(13)  
  (2) a.getSome()



 

情形三:讀同步,寫不一樣步 jvm

Thread1 Thread2
(1) a.setSome(13)  
  (2) a.getSomeWithSync()



 

在這兩種狀況下,thread1和thread2只對讀或只對寫some加了鎖,這不起任何做用,和[情形一]同樣,thread2仍有可能讀到some的初始值0。從圖上也可看出,thread1和thread2互相之間並無任何影響,一方加鎖並不影響另外一方的繼續運行。圖中也顯示,同步操做至關於在同步開始執行lock操做,在同步結束時執行unlock操做。 優化

情形四:讀寫都同步 this

Thread1 Thread2
(1) a.setSomeWithSync(13)  
  (2) a.getSomeWithSync()



 

在情形四中,thread1寫入some時,thread2等待thread1寫入完成,而且它能看到thread1對some作的修改,這時thread2保證能讀到13。實際上,thread2不只能看到thread1對some的修改,並且還能看到thread1在修改some以前所作的任何修改。說得更精確一些,就是一個線程的lock操做能看見另外一線程對同一個對象unlock操做以前的全部修改,請注意圖中的紅色箭頭。 沿着圖中箭頭指示方向,箭頭結尾處總能看到箭頭開始處操做作的修改。這樣,a.some[thread2]能看見lock[thread2],lock[thread2]能看見unlock[thread1],unlock[thread1]又能看見a.some=13[thread1],即能看到some的值爲13。 

再來看一個稍微複雜一點的例子: 線程

例子五 設計

Thread1 Thread2
(1) a.another = 5  
(2) a.setSomeWithSync(13)  
  (3) a.getSomeWithSync()
(4) a.another = 7  
  (5) a.another



 

thread2最後會讀到another的什麼值呢?會不會讀到another的初始值0呢,畢竟全部對another的訪問都沒有同步?不會。從圖中很清晰地能夠看出,thread2的another至少到看到thread1在lock以前寫入的5,卻並不能保證它能看到thread1在unlock寫入的7。所以,thread2能夠什麼讀到another的值可能5或7,但不會是0。你或許已經發現,若是去掉圖中thread2讀取a.some的操做,這時至關於一個空的同步塊,對結論並無任何影響。這說明空的同步塊是起做用的,編譯器不能擅自將空的同步塊優化掉,但你在使用空的同步塊應該特別當心,一般它都不是你想要的結果。另外須要注意,unlock操做和lock操做必須針對同一個對象,才能保證unlock操做能看到lock操做以前所作的修改。 

例子六:不一樣的鎖 

class B {
	private Object lock1 = new Object();
	private Object lock2 = new Object();

	private int some;

	public int getSome() {
		synchronized(lock1) { return some; }
	}

	public void setSome(int v) {
		synchronized(lock2) { some = v; }
	}
}

 

Thread1 Thread2
(1) b.setSome(13)  
  (2) b.getSome()



 

在這種狀況下,雖然getSome和setSome都加了鎖,但因爲它們是不一樣的鎖,一個線程運行時並不能阻塞另外一個線程運行。所以這裏的情形和情形1、2、三同樣,thread2不保證讀到thread1寫入的some最新值。 

如今來看DCL: 

例子七: DCL 

public class LazySingleton {
    private int someField;
    
    private static LazySingleton instance;
    
    private LazySingleton() {
        this.someField = 201;                                 // (1)
    }
    
    public static LazySingleton getInstance() {
        if (instance == null) {                               // (2)
            synchronized(LazySingleton.class) {               // (3)
                if (instance == null) {                       // (4)
                    instance = new LazySingleton();           // (5)
                }
            }
        }
        return instance;                                      // (6)
    }
    
    public int getSomeField() {
        return this.someField;                                // (7)
    }
}

  假設thread1先調用getInstance(),因爲此時尚未任何線程建立LazySingleton實例,它會建立一個實例s並返回。這是thread2再調用getInstance(),當它運行到(2)處,因爲這時讀instance沒有同步,它有可能讀到s或者null(參考情形二)。先考慮它讀到s的情形,畫出流程圖就是下面這樣的: 

 

因爲thread2已經讀到s,因此getInstance()會當即返回s,這是沒有任何問題,但當它讀取s.someFiled時問題就發生了。 從圖中能夠看thread2沒有任何同步,因此它可能看不到thread1寫入someField的值20,對thread2來講,它可能讀到s.someField爲0,這就是DCL的根本問題。從上面的分析也能夠看出,爲何試圖修正DCL但又但願徹底避免同步的方法幾乎老是行不通的。 

接下來考慮thread2在(2)處讀到instance爲null的情形,畫出流程圖: 

 

接下來thread2會在有鎖的狀況下讀取instance的值,這時它保證能讀到s,理由參考情形四或者經過圖中箭頭指示方向來斷定。 

關於DCL就說這麼多,留下兩個問題: 

    1. 接着考慮thread2在(2)讀到instance爲null的情形,它接着調用s.someFiled會獲得什麼?會獲得0嗎?
    2. DCL爲何要double check,能不能去掉(4)處的check?若不能,爲何?

 

原子性 
回到情形一,爲何咱們說thread2讀到some的值只可能爲爲0或13,而不可能爲其它?這是由java對int、引用讀寫都是原子性所決定的。所謂「原子性」,就是不可分割的最小單元,有數據庫事務概念的同窗們應該對此容易理解。當調用some=13時,要麼就寫入成功要麼就寫入失敗,不可能寫入一半。可是,java對double, long的讀寫卻不是原子操做,這意味着可能發生某些極端意外的狀況。看例子: 

public class C {
	private /* volatile */ long x;                           // (1)

	public void setX(long v) { x = v; }
	public long getX() { return x; }
}

  

Thread1 Thread2
(1) c.setX(0x1122334400112233L)  
  (2) c.getX()



thread2讀取x的值可能爲0,1122334400112233外,還可能爲別的徹底意想不到的值。一種狀況假設jvm對long的寫入是先寫低4字節,再寫高4字節,那麼讀取到x的值還可能爲112233。可是咱們不對jvm作如此假設,爲了保證對long或double的讀寫是原子操做,有兩種方式,一是使用volatile,二是使用synchronized。對上面的例子,若是取消(1)處的volatile註釋,將能保證thread2讀取到x的值要麼爲0,要麼爲1122334400112233。若是使用同步,則必須像下面這樣對getX,setX都同步:

public class C {
	private /* volatile */ long x;                           // (1)

	public synchronized void setX(long v) { x = v; }
	public synchronized long getX() { return x; }
}

所以對原子性也有規則(volatile其實也是一種同步)。 

規則二:對double, long變量,只有對全部讀寫都同步,才能保證它的原子性 

有時候咱們須要保證一個複合操做的原子性,這時就只能使用synchronized。 

public class Canvas {
	private int curX, curY;

	public /* synchronized */ getPos() {
		return new int[] { curX, curY };
		
	}

	public /* synchronized */ void moveTo(int x, int y) {
		curX = x;
		curY = y;
	}
}

  

Thread1 Thread2
(1) c.moveTo(1, 1)  
(2) c.moveTo(2, 2)  
  (3) c.getPos()



當沒有同步的狀況下,thread2的getPos可能會獲得[1, 2], 儘管該點可能歷來沒有出現過。之因此會出現這樣的結果,是由於thread2在調用getPos()時,curX有0,1或2三種可能,一樣curY也有0,1或2三種可能,因此getPos()可能返回[0,0], [0,1], [0,2], [1,0], [1,1], [1,2], [2,0], [2,1], [2,2]共九種可能。要避免這種狀況,只有將getPos()和moveTo都設爲同步方法。 

總結 
以上分析了數據競爭的兩種症狀,陳舊數據和非原子操做,都是因爲沒有恰當同步引發的。這些其實都是至關基礎的知識,同步可有兩種效果:一是保證讀取最新數據,二是保證操做原子性,可是大多數書籍都對後者過份強調,對前者認識不足,以至對多線程的認識上存在不少誤區。若是想要掌握java線程高級知識,我只推薦《Java併發編程設計原則與模式》。其實我已經很久沒有寫Java了,這些東西都是我兩年前的知識,若是存在問題,歡迎你們指出,千萬不要客氣。 

 還有一篇講的也很不錯:http://www.iteye.com/topic/260515

相關文章
相關標籤/搜索