java多線程併發(二)(線程基礎)

轉自:http://blog.psjay.com/posts/summary-of-java-concurrency-two-synchronized-and-atomicity/

再來考慮一下前幾天發生的事情。因爲日本地震海嘯以及核爆炸的緣故,有人造謠說,咱國內已經受到了核污染,喫含碘的東西能夠減輕核輻射帶來的影響。於是就有投機的人在淘寶上開了一家網店,專賣碘片,一塊錢一片。生意十分火爆。有很多個買家不斷地在買碘片,一直到把錢給用光。買家買碘片的這些錢都打到了賣家的同一個銀行賬號裏。所以,結果就是,買家所有的錢最後都到了賣家的銀行賬戶裏,賣家銀行賬號裏的總額就是所有買家在買碘片之前的現金總計。

所以可以這樣來設計類:

/**
 * 銀行賬戶類
 * 
 */
public class BankAccount {

	private int total = 0;

	public void add(int n) {
		total += n;
	}

	public int getTotal() {
		return total;
	}
}

/**
 * 買家類
 * 
 */

public class Customer implements Runnable {

	private BankAccount account;
	private int cash;

	public Customer(int cash, BankAccount account) {
		this.cash = cash;
		this.account = account;
	}

	public void cost(int n) {
		cash -= n;
		account.add(n);
	}

	@Override
	public void run() {
		while (cash > 0) { // 直至將錢用光
			cost(1);
		}
		System.out.println("total: " + account.getTotal()); // 打印出銀行賬戶的總計金額
	}
}

public class Test {

	public static void main(String[] args) {

		BankAccount account = new BankAccount();
		for (int i = 0; i < 100; i++) {
			new Thread(new Customer(100000, account)).start();
		}
	}

}

正如代碼所示,有100個聰明的嚮往健康長壽的又有錢的買家各自用10萬塊不斷地狂買碘片。

你可能注意到了,BankAccount類的add()方法並不是synchronized的。爲什麼呢?因爲add()方法裏只有一句話,那就是“total += n;”,所以不會出現第一個例子中那樣多句話執行到一半被打斷的問題。這是正確的麼?實踐是檢驗真理的唯一標準,上述程序的某次運行結果如下:

(省略了N行)
total: 7861909
total: 7881906
total: 7995946
total: 8001495
total: 8081441

oops!居然出問題了!最後賣家銀行賬戶裏的總額並不是100*100000 = 10000000。看吧,無故少了這麼多錢,有誰會願意去這樣的“吞錢”銀行開設賬戶呢?那麼問題究竟出在哪裏了呢?BankAccount的add()方法不是隻有一句話麼,難道這一句話也能被打斷?回答就是:這一句話確實能夠被打斷,因爲這樣的操作不具有原子性(atomicity),關於原子性稍後再總結。先談談怎麼解決這個銀行吞錢的問題。當然,如上面所說的,給add()方法和getTotal()加上synchronized就行了。

synchronized除了能修飾方法之外,還能創建同步塊。如果有時候一個方法裏面只有幾句話需要同步,你可以考慮這種寫法:

public void doSomething() {
		// 一些操作
		synchronized (this) {
			// 一些需要被同步的操作
		}
		// 另外一些操作
	}

其中,synchronized後的括號內必須要爲一個對象。表示:要執行同步塊裏的這些操作一定要當前線程取得括號內的這個對象的鎖纔行。常用的就是this,表示當前對象。在一些高級應用中,可能會用到其他對象的鎖。

你也可以顯式的使用鎖對象來實現同步,Java提供了一些Lock類,本篇總結中不打算包含這些內容。

原子性(atomicity)

具有原子性的操作被稱爲原子操作。原子操作在操作完畢之前不會線程調度器中斷。在Java中,對除了long和double之外的基本類型的簡單操作都具有原子性。簡單操作就是賦值或者return。比如”a = 1;“和 “return a;”這樣的操作都具有原子性。但是在Java中,上面買碘片例子中的類似”a += b”這樣的操作不具有原子性,所以如果add方法不是同步的就會出現難以預料的結果。在某些JVM中”a += b”可能要經過這樣三個步驟:

  1. 取出a和b

  2. 計算a+b

  3. 將計算結果寫入內存

如果有兩個線程t1,t2在進行這樣的操作。t1在第二步做完之後還沒來得及把數據寫回內存就被線程調度器中斷了,於是t2開始執行,t2執行完畢後t1又把沒有完成的第三步做完。這個時候就出現了錯誤,相當於t2的計算結果被無視掉了。所以上面的買碘片例子在同步add方法之前,實際結果總是小於預期結果的,因爲很多操作都被無視掉了。

類似的,像”a++“這樣的操作也都不具有原子性。所以在多線程的環境下一定要記得進行同步操作。有一些併發大牛可以利用原子性避免同步而寫出“免鎖”的代碼。Goetz開玩笑說:

如果你能編寫出一個牛逼的高性能的JVM,你就可以考慮考慮是否可以避免使用同步。

所以,在成爲這樣牛的大牛之前,還是老老實實使用同步吧。

Java SE引入了原子類,比如AtomicInter,AtomicLong等等。

volatile

上面提到了,對long和double的簡單操作不具有原子性。但是,一旦給這兩個類型的屬性加上volatile修飾符,對它們的簡單操作就會具有原子性(當然這是說的在Java SE5之後的故事)。

在一些情況下即便是原子操作也可能會引發一些錯誤,特別是在多處理器的環境下。因爲多處理器的計算機可以將內存中的值暫時儲存在寄存器或者本地內存緩衝區中。所以,運行在不同處理器上的線程取同一個內存位置的值可能不相同。有一些編譯器也會自作主張地優化指令,使得上述情況發生。你當然可以用同步鎖來解決這些問題,不過volatile也能解決。

如果給一個變量加上volatile修飾符,就相當於:每一個線程中一旦這個值發生了變化就馬上刷新回主存,使得各個線程取出的值相同。編譯器不要對這個變量的讀、寫操作做優化。

但是值得注意的是,除了對long和double的簡單操作之外,volatile並不能提供原子性。所以,就算你將一個變量修飾爲volatile,但是對這個變量的操作並不是原子的,在併發環境下,還是不能避免錯誤的發生!比如在碘片例子中,將BankAccount類寫成這樣:

即便total被volatile修飾,但是由於add方法不是同步的,所以不能避免錯誤的發生!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章