Java併發基礎03:傳統執行緒的互斥技術—synchronized

公眾號_程式設計師私房菜發表於2019-01-08

歡迎關注我的微信公眾號:程式設計師私房菜(id:eson_15)

在多個執行緒同時操作相同資源的時候,就會遇到併發的問題,如銀行轉賬啊、售票系統啊等。為了避免這些問題的出現,我們可以使用 synchronized 關鍵字來解決,下面針對 synchronized 常見的用法做一個總結。首先寫一個存在併發問題的程式,如下:

public class TraditionalThreadSynchronized {

	public static void main(String[] args) {
		//在靜態方法中不能new內部類的例項物件
		//private Outputer outputer = new Outputer();
		new TraditionalThreadSynchronized().init();
	}
	
	private void init() {
		final Outputer outputer = new Outputer();
		//執行緒1列印:duoxiancheng
		new Thread(new Runnable() {			
			@Override
			public void run() {
				while(true) {
					try {
						Thread.sleep(5);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					outputer.output1("duoxiancheng");
				}
				
			}
		}).start();;
		
		//執行緒2列印:eson15
		new Thread(new Runnable() {			
			@Override
			public void run() {
				while(true) {
					try {
						Thread.sleep(5);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					outputer.output1("eson15");
				}
				
			}
		}).start();;
	}
	
	static class Outputer {
		//自定義一個字串列印方法,一個個字元的列印
		public void output1(String name) {
			int len = name.length();
			for(int i = 0; i < len; i++) {
				System.out.print(name.charAt(i));
			}
			System.out.println("");		
		}		
	}
}
複製程式碼

在內部類 Outputer 中定義了一個列印字串的方法,一個字元一個字元的列印,這樣比較容易直觀的看出併發問題,因為字元順序打亂了就說明出現問題了。然後在init方法中開啟兩個執行緒,一個執行緒列印“duoxiancheng”,另一個執行緒列印“eson15”。看一下執行結果:

eson15duoxianche ng eson15 duoxiancheng duoxiancheng eson15 esduoxiancheng on15 duoxiancheng

已經出現問題了,為了解決這個問題,可以使用 synchronized 同步程式碼塊來對公共部分進行同步操作,但是需要給它一把鎖,這把鎖是一個物件,可以是任意一個物件,但是前提是,兩個執行緒使用的必須是同一個物件鎖才可以,這很好理解。那麼下面在 output1() 方法中加入 synchronized 程式碼塊:

static class Outputer {
	private String token = ""; //定義一個鎖
	public void output1(String name) {
		synchronized(token) //任何一個物件都可以作為引數,但是該物件對於兩個執行緒來說是同一個才行
		//如果用name就不行了,因為不同的執行緒進來name是不一樣的,不是同一個name
		{
			int len = name.length();
			for(int i = 0; i < len; i++) {
				System.out.print(name.charAt(i));
			}
			System.out.println("");		
		}
	}
}	
複製程式碼

經過上面的改造,執行緒安全問題就基本解決了,但是還可以再往下引申,如果在方法上加 synchronized 關鍵字的話,那麼這個同步鎖是什麼呢?我們在 Outputer 類中再寫一個output2()方法:

static class Outputer {
	private String token = ""; //定義一個鎖
	public void output1(String name) {
		synchronized(token) //任何一個物件都可以作為引數,但是該物件對於兩個執行緒來說是同一個才行
		{
			int len = name.length();
			for(int i = 0; i < len; i++) {
				System.out.print(name.charAt(i));
			}
			System.out.println("");		
		}
	}	
	
	public synchronized void output2(String name) {
		
		int len = name.length();
		for(int i = 0; i < len; i++) {
			System.out.print(name.charAt(i));
		}
		System.out.println("");		
	}	
}
複製程式碼

方法內部實現邏輯一模一樣,唯一不同的就是 synchronized 加在了方法上,那麼我們讓 init() 方法中的兩個執行緒中,一個呼叫 output1(String name) 方法,另一個呼叫 output2(String name) 方法,從結果中能看出,執行緒安全問題又出現了。產生問題的原因不難發現:現在兩個方法都加了 synchronized,但是兩個執行緒在呼叫兩個不同的方法還是出現了問題,也就是說,還是各玩各的……那麼問題就出在這個鎖上,說明兩者並沒有使用同一把鎖!

如果我們把 output1() 方法中 synchronized 中的 token 改成 this,再執行就沒問題了,這說明一點:synchronized關鍵字修飾方法的時候,同步鎖是 this,即等效於程式碼塊synchronized(this) {...}

再繼續往下引申,現在在 Outputer 類中再寫一個靜態方法output3(String name),並且也讓 synchronized 去修飾這個靜態方法。

static class Outputer {
	private String token = ""; //定義一個鎖
	public void output1(String name) {
		synchronized(token) //任何一個物件都可以作為引數,但是該物件對於兩個執行緒來說是同一個才行
		{
			int len = name.length();
			for(int i = 0; i < len; i++) {
				System.out.print(name.charAt(i));
			}
			System.out.println("");		
		}
	}	
	
	public static synchronized void output3(String name) {
		
		int len = name.length();
		for(int i = 0; i < len; i++) {
			System.out.print(name.charAt(i));
		}
		System.out.println("");		
		}	
	}
}
複製程式碼

然後在 init() 方法中一個執行緒呼叫 output1() 方法,另一個執行緒呼叫 output3() 方法。毫無疑問,肯定又會出現執行緒安全問題。但是如何解決呢?因為 static 方法在類載入的時候就載入了,所以這個鎖應該是類的位元組碼物件。那麼將 token 改為 Outputer.class 就解決問題了,這說明一點:synchronized關鍵字修飾static方法的時候,同步鎖是該類的位元組碼物件,即等效於程式碼塊 synchronized(classname.class) {...}。    最後再總結一下:

  • 同步程式碼塊的鎖是任意物件。只要不同的執行緒都執行同一個同步程式碼塊的時候,這個鎖隨便設。
  • 同步函式的鎖是固定的this。當需要和同步函式中的邏輯實行同步的時候,程式碼塊中的鎖必須為this。
  • 靜態同步函式的鎖是該函式所屬類的位元組碼檔案物件。該物件可以用 this.getClass() 方法獲取,也可以使用 當前類名.class 表示。

OK,synchronized 就總結這麼多~如有錯誤之處,歡迎指正~我們一起進步!

也歡迎大家關注我的微信公眾號:程式設計師私房菜。我會持續輸出更多文章。

公眾號

相關文章