Effective Java 讀書筆記——66:同步訪問共享的可變數據

關鍵字synchronized可以保證同一時刻,只有一個線程可以執行某個方法。

同步的概念

1、當一個對象被一個線程修改的時候,可以阻止另一個線程觀察到對象內部不一致的狀態;

2、同步不僅可以組織一個線程看到對象處於不一致的狀態,還可以保證進入同步方法或者同步代碼塊的每個線程,都看到由同一個鎖保護的之前所有的修改效果。

另外,java語言規範保證讀寫一個變量是原子的,除非這個變量是double或者long(JSL,17.4.7),即使沒有在保證同步的情況下也是如此。


"爲了提高性能,在讀寫原子數據的時候,應該避免使用同步。”這個建議是非常危險而且錯誤的。因爲,雖然讀寫原子數據都是原子操作,但是不保證一個線程的寫入的值對於另一個線程是完全可見的(值得一提的是,究竟什麼樣的原子變量必須進行同步,是需要看情況的)。因此,爲了在線程之間進行可靠的通信,也爲了互斥訪問,同步是必要的。


考慮一個線程妨礙另一個線程任務

如果需要停止一個線程,可以使用Thread.stop方法,但是這個方法很久以前就不提倡使用了,因爲不安全——使用它會使數據被破壞。
因此,建議的做法是,讓一個線程輪詢一個boolean域,另一個線程設置這個boolean域即可:
public class Temp {

	private static boolean stopRequested;

	public static void main(String[] args) throws InterruptedException {
		Thread backgroundThread = new Thread(new Runnable() {
			@Override
			public void run() {
				int i = 0;
				while (!stopRequested) {
					i++;
				}
			}
		});
		backgroundThread.start();
		TimeUnit.SECONDS.sleep(1);
		stopRequested = true;
	}

}
實際上以上這段程序會永遠的運行下去,因爲沒有使用同步,無法保證後臺進程可以看到stopRequested值的改變。虛擬機將代碼:
				while (!stopRequested) {
					i++;
				}
轉變成了:
if(!stopRequested)
    while(true)
        {
            i++;
        }
這樣一來,永遠都不會看到stopRequested的改變。
必須讓線程看到變量的改變纔好,因此使用同步修改以上的代碼:
	private static boolean stopRequested;

	private static synchronized void requestStop() {
		stopRequested = true;
	}

	private static synchronized boolean stopRequested() {
		return stopRequested;
	}

	public static void main(String[] args) throws InterruptedException {
		Thread backgroundThread = new Thread(new Runnable() {
			@Override
			public void run() {
				int i = 0;
				while (!stopRequested()) {
					i++;
				}
			}
		});
		backgroundThread.start();
		TimeUnit.SECONDS.sleep(1);
		requestStop();
	}
事實上,在本例中,只同步讀方法(這和下面的volatile類似),不同步寫方法,也是可以的。

使用volatile

在上面的例子裏面,使用同步進行的,我們同步是爲了保證通信效果,而不是互斥,所以可以使用volatile來實現相同的功能:
	private static volatile boolean stopRequested;

	public static void main(String[] args) throws InterruptedException {
		Thread backgroundThread = new Thread(new Runnable() {
			@Override
			public void run() {
				int i = 0;
				while (!stopRequested) {
					i++;
				}
			}
		});
		backgroundThread.start();
		TimeUnit.SECONDS.sleep(1);
		stopRequested = true;
	}
在使用volatile的時候要非常小心,以下的例子說明它可能會出現錯誤:
	private static volatile int nextSerialNumber = 0;

	public static int generateSerialNumber() {
		return nextSerialNumber++;
	}
儘管使用了volatile,但是由於++運算符不是原子的,因此在多線程的時候會出錯。++運算符執行兩項操作:1、讀取值;2、寫回新值(相當於原值+1)。如果第二個線程在第一個線程讀取舊值和寫會新值的時候讀取了這個域,就會產生錯誤,他們會得到相同的SerialNumber。
解決方法可以是,加入synchronized並去掉volatile。進一步的,可以用Long來代替int,或者在快要溢出的時候,拋出異常。更好的是使用AtomicLong類。
	private static final AtomicLong nextSerialNumber = new AtomicLong(0);

	public static Long generateSerialNumber() {
		return nextSerialNumber.incrementAndGet();
	}


總結

解決這一問題的最好辦法其實是儘量避免在線程間共享可變數據,將可變數據限制在單線程中。讓線程短時間修改一個對象,並與其他線程共享,這是可以接受的,只需要同步修改的動作即可。不需要同步,其他線程也可以讀取該對象,只要它以後沒有再改變。總的來說,如果想要多個線程共享可變數據,那麼讀寫都需要進行同步。


發佈了55 篇原創文章 · 獲贊 12 · 訪問量 21萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章