使用synchronized需要注意的一個問題

我們知道,在java中,每個對象都持有一把對象鎖,這是在java語言層實現的機制(不同於java.util.concurrent包的lock),因此不需要顯式去釋放鎖。

當我們談到synchronized這個關鍵字,大夥第一時間想到的,可能就是:當資源共享時,訪問(修改)該資源的所有方法都要加上鎖(synchronized),才能保證數據不會出錯(數據不一致);

Java提供了專門的機制去避免了同一個數據對象被多個線程同時訪問,這套機制就是 synchronized 關鍵字,它包括兩種用法:synchronized 方法和 synchronized 塊。

1. synchronized 方法:通過在方法聲明中加入 synchronized關鍵字來聲明 synchronized 方法。如:

public synchronized void foo(int i);

2. synchronized 塊:通過 synchronized關鍵字來聲明synchronized 塊。語法如下:
synchronized(syncObject) {
      //  允許訪問控制的代碼
      //  其中的代碼塊必須獲得對象syncObject的鎖才能執行
}

如果方法A和方法B都對同一個對象加了synchronized,並且在方法A中調用了方法B;假設有某個線程T調用方法A,線程T也不需要在A調用方法B時等待該 對象鎖 釋放從而在方法B中又持有該對象的鎖,因爲synchronized 不是方法級的,而是線程級的;因此,線程T在A方法的時候,就已經獲得了那個對象的鎖了。

而我們這裏所要探討的,就是synchronized塊的參數;而synchronized 塊裏的代碼必須要獲得對象 syncObject (可以是類實例或類)的鎖方能執行。


首先我們來模擬兩個線程,逐個字母輸出某個字符串:

package com.gdut.thread;

public class SynchronizedTest {
	
	public static void main(String[] args) {
		new SynchronizedTest().init();
	}
	
	public void init() {
		
		final Outputer outputer = new Outputer();
		
		// 第一個線程
		new Thread(new Runnable() {
			
			@Override
			public void run() {
				while(true) {
					try {
						Thread.sleep(5);
					} catch (Exception e) {
						e.printStackTrace();
					}
					
					// 輸出某個字符串
					// 在這個匿名類裏,我們需要把outputer變量定義爲final
					outputer.output("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
				}
			}
		}).start();
		

		// 第二個線程
		new Thread(new Runnable() {
			
			@Override
			public void run() {
				while(true) {
					try {
						Thread.sleep(50);
					} catch (Exception e) {
						e.printStackTrace();
					}
					
					// 輸出某個字符串
					// 在這個匿名類裏,我們需要把outputer變量定義爲final
					outputer.output("5555555555555555555");
				}
			}
		}).start();
		
	} // end of init
	
	class Outputer {
		public void output(String name) {
			int length = name.length();
			
			for(int i=0; i<length; i++) {
				// 逐個字母輸出
				System.out.print(name.charAt(i));
			}
			System.out.println();
		}
	}
	
}

上面這段程序是線程不安全的,我們來看看它的部分輸出:


對此,我們有一種比較省事的做法,直接在Output類的output方法加上關鍵字synchronized,即:public synchronized void output(String name) { }


我們也還有另外一種方法,就是synchronized塊,而這裏便涉及到synchronized塊裏面的參數;

通俗一點的說法就是:

當A線程執行到synchronized塊的時候,A會去檢查參數對象的鎖有沒有給其他線程拿走,如果沒拿走,則線程A拿走該對象的鎖,並且在線程A執行unlock前不允許其他線程執行該對象鎖的所有synchronied塊。

但是下面這段代碼同樣是線程不安全的,原因就在synchronized塊的參數name是一個局部變量,也就是說,當A,B兩個線程執行到這的時候,都會去執行synchronized塊裏面的代碼!

class Outputer {
		public void output(String name) {
			int length = name.length();
			
			synchronized(name) {
				for(int i=0; i<length; i++) {
					// 逐個字母輸出
					System.out.print(name.charAt(i));
				}
				System.out.println();
			}
		}
	}

運行結果:


把參數對象改成" this " 或者是Outputer類的成員變量str,都可以解決這個問題:

	class Outputer {
		// String str = "";
		public void output(String name) {
			int length = name.length();

			
			// synchronized(str) {

			// 這裏的"this"表示調用該方法的那個對象,即init方法的 final Outputer outputer = new Outputer();

			// 如果A,B線程各自new Outputer()對象,這樣又是不同步的了!
			synchronized(this) {
				for(int i=0; i<length; i++) {
					// 逐個字母輸出
					System.out.print(name.charAt(i));
				}
				System.out.println();
			}
		}
	}


===============================

接下來,我們繼續看一下下面的這個例子:

package com.gdut.thread;

public class SynchronizedTest {
	
	public static void main(String[] args) {
		new SynchronizedTest().init();
	}
	
	public void init() {
		
		final Outputer outputer = new Outputer();
		
		// 第一個線程
		new Thread(new Runnable() {
			
			@Override
			public void run() {
				while(true) {
					try {
						Thread.sleep(10);
					} catch (Exception e) {
						e.printStackTrace();
					}
					
					// 輸出某個字符串
					// 調用的是output_1
					outputer.output_1("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
				}
			}
		}).start();
		

		// 第二個線程
		new Thread(new Runnable() {
			
			@Override
			public void run() {
				while(true) {
					try {
						Thread.sleep(10);
					} catch (Exception e) {
						e.printStackTrace();
					}
					
					// 輸出某個字符串
					// 調用的是output_2
					outputer.output_2("5555555555555555555");
				}
			}
		}).start();
		
	} // end of init
	

	static class Outputer {
		
		public void output_1(String name) {
			int length = name.length();
			
			synchronized (this) {
				for(int i=0; i<length; i++) {
					// 逐個字母輸出
					System.out.print(name.charAt(i));
				}
				System.out.println();
			}
		}
		
		public static synchronized void output_2(String name) {
			int length = name.length();
			
			for(int i=0; i<length; i++) {
				// 逐個字母輸出
				System.out.print(name.charAt(i));
			}
			System.out.println();
		}
	}
	
}

output_1和output_2的區別是:前者是synchronized塊,而後者是static方法!輸出的結果同樣是有問題的,如下:

也就是說,和static方法關聯的對象,並不是this,而是某個Class類!(這裏的的Class爲:java.lang.Class)把output_1方法稍做修改,即可:

public void output_1(String name) {
			int length = name.length();
			// 參數對象爲Outputer.class
			synchronized (Outputer.class) {
				for(int i=0; i<length; i++) {
					// 逐個字母輸出
					System.out.print(name.charAt(i));
				}
				System.out.println();
			}
}

這樣同步的問題又解決了!!

在最後,建議大夥都學習一下java.util.concurrent包裏面的相關知識!

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