Java基礎--併發編程基礎(2)

1.線程同步

爲什需要線程同步?當多個線程訪問互斥資源(不一定是互斥資源,是需要限制訪問的資源,這裏舉一個互斥資源的例子)時,每次只能有一個線程進行訪問,這個過程叫做線程同步。譬如:你的筆記本壞了,筆記本是互斥資源要麼你在用,要麼給店員維修:當店員維修的時候,你不應該使用,當你使用的時候,店員不應該維修,這個場景即線程同步。
Java中實現線程同步有兩種最基本方式(後續博文會探討其他線程同步的實現),一種是使用同步代碼塊,一種是使用同步方法。各有各的用處:如果源碼是你自己寫的話,使用同步方法直接定義就是OK的,但是如果你要進行併發的操作並不是你編寫的而是第三方的,你看不到源碼,這個時候就要使用同步代碼塊咯。
同步方法:在方法原型上加上synchronized關鍵字即可,實例代碼如下:
public class SynchronizeThreadsUsingSynchronizedMethod {

	public static void main(String[] args) {
		/*
		 * 線程同步是爲了解決當多個線程同時訪問互斥資源時如何保證只有一個線程在使用資源,資源可以是任何東西
		 */
		CallMe callMe = new CallMe();
		new Thread(()->{
			Caller caller = new Caller(callMe,"hello");
			caller.call();
		}).start();
		new Thread(()->{
			Caller caller = new Caller(callMe,"world");
			caller.call();
		}).start();
		new Thread(()->{
			Caller caller = new Caller(callMe,"I'm Max");
			caller.call();
		}).start();
		/*
		 * 對方法進行同步之前,輸出結果:
		 * [hello[world[I'm Max]
		 * ]
		 * ]
		 */
		/*
		 * 對方法加了同步之後,輸出結果:
		 * [hello]
		 * [I'm Max]
		 * [world]
		 */
	}
	
}
class CallMe{
	public synchronized void showMsg(String msg){
		System.out.print("["+msg);
		try {
			//睡上這一秒就是爲了在線程不同步的時候讓其他線程插進來
			Thread.sleep(1000);
			System.out.println("]");
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

class Caller{
	private CallMe callMe;
	private String msg;
	public Caller(CallMe callMe,String msg){
		this.callMe = callMe;
		this.msg = msg;
	}
	public void call(){
		callMe.showMsg(msg);
	}
}
同步代碼塊:格式爲synchronized(objRef){},objRef爲不是併發設計但是你想併發使用的對象,實例如下:
public class SynchronizeThreadsUsingSynchronizedStatements {

	public static void main(String[] args) {
		/*
		 * 有時候,因爲有些類本身並不是爲了併發而設計的,但是你又想併發使用,巧的是你不是該類的作者即不能修改該類的源碼,那這個時候同步語句的作用就來咯
		*/
		CallMe2 callMe = new CallMe2();
		new Thread(()->{
			Caller2 caller = new Caller2(callMe,"hello");
			caller.call();
		}).start();
		new Thread(()->{
			Caller2 caller = new Caller2(callMe,"world");
			caller.call();
		}).start();
		new Thread(()->{
			Caller2 caller = new Caller2(callMe,"I'm Max");
			caller.call();
		}).start();
		/*
		 * 使用同步語句前:
		 * [world[hello[I'm Max]
		 * ]
		 * ]
		 * 使用同步語句後:
		 * [world]
		 * [I'm Max]
		 * [hello]
		 */
	}
}
class CallMe2{
	public void showMsg(String msg){
		System.out.print("["+msg);
		try {
			Thread.sleep(1000);
			System.out.println("]");
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

class Caller2{
	private CallMe2 callMe;
	private String msg;
	public Caller2(CallMe2 callMe,String msg){
		this.callMe = callMe;
		this.msg = msg;
	}
	public void call(){
		synchronized(callMe){
			callMe.showMsg(msg);
		}
	}
}

2.Java中的監視器

你肯定想過這個問題:wait(),notify(),notifyAll()(進行線程間通信的方法)方法爲什麼被設計爲Object的不可重寫方法,而不是Thread類的方法呢?回答這個問題和學習線程間通信之前,先了解一下什麼是『監視器』?
監視器(monitor):相當於一個小房子,放到這個監視器小房子中的東西每次都只能由一個消費者使用,那反映到Java中,每個對象都有自己的數據和操作,有時候我們就想實現一個對象中的數據或操作在任意時候都只能由一個線程訪問,那就要用到Java中的監視器了,Java在官方文檔中用的也是monitor這個單詞哦,所以要理解好這個monitor是個什麼鬼。
如果要我們自己爲每個對象都實現一個監視器太過麻煩了,Java爲每個對象(不是類,是對象)都內置關聯了一個監視器,同樣的,如果不告訴監視器這個對象的哪個部分是被監視的,監視器是不起作用的,而要怎麼告訴監視器這個對象中哪些數據和操作是要被監視的呢?是使用同步方法和同步代碼塊的方式。
問題又來了,一個對象中可能有多個同步代碼塊和同步方法,那這些同步代碼塊和同步方法之內應該是線程同步的,那之間呢?可不可以多個線程同時訪問這個對象中不同的同步代碼塊或同步方法呢?答案是否定的:不能。每個對象只被關聯一個監視器,而這個監視器每次只能由一個線程持有,也就是說:一個對象中無論有幾個同步代碼塊和同步方法,他們都是一個整體,都被放到了監視器這個小屋子內,而線程對這個對象的訪問是是否拿到了監視器。
驗證示例代碼如下:
public class TestJavaMonitor {

	public static void main(String[] args) {
		TestJavaMonitor_T1 t1 = new TestJavaMonitor_T1();
		new Thread(()->{
			t1.method1();
		}).start();
		new Thread(()->{
			t1.method2();
		}).start();
		new Thread(()->{
			t1.method1();
		}).start();
		/*
		 * 目的:爲了檢驗一個對象中的不用同步塊或同步方法之間是否是互斥的
		 * 起因:Java爲每一個對象都關聯了一個監視器,如果不爲該監視器指明要監視的內容,則該監視器不起作用
		 * 		使用同步塊或者同步語句來告訴該對象的監視器要監視的內容
		 * 		每個對象都只關聯了一個監視器,而這個監視器可以監視多個區域(數據或操作)
		 * 		每個進入到監視區域(同步塊或者同步方法)內的線程將持有該監視器
		 * 		但是wait之後,表示該線程被掛起,釋放監視器
		 * 		notify是喚醒一個或多個(不同方法)等待持有該監視器的線程
		 * 		監視器的作用就是該監視器下的對象資源每次只能被一個線程所訪問
		 * 		所以,同一對象中的不同同步塊之間的運行同步的,因爲這些同步塊和同步方法中的內容是由該對象的同一個也是唯一一個監視器所監視的
		 * 		也就是說:即使有多個線程訪問該對象的不同同步塊,每次也只能有一個對象訪問該對象
		 * 		更簡而言之:每個對象的監視器中的內容每個時刻都只能被一個線程所訪問
		 * 		不被監視的內容就沒有這個限制了
		 * 結論:是的,因爲位於同一個監視器下
		 */
//		運行結果如下:
//		這個結果並不能證明什麼,但是我發誓,運行了至少20遍,都是一個方法執行完了之後,纔會去執行另一個方法,如果可以多個線程同時調用對象不同的同步塊,20次都是這樣一個方法執行完一個方法纔開始執行的可能性幾乎爲零
//		method1...1
//		method1...2
//		method1...3
//		method1...4
//		method1...5
//		method1...6
//		method1...7
//		method1...8
//		method1...9
//		method1...10
//		method1...11
//		method1...12
//		method1...13
//		method1...14
//		method1...15
//		method1...16
//		method1...17
//		method1...18
//		method1...19
//		method1...20
//		method2...19--------------->
//		method2...18--------------->
//		method2...17--------------->
//		method2...16--------------->
//		method2...15--------------->
//		method2...14--------------->
//		method2...13--------------->
//		method2...12--------------->
//		method2...11--------------->
//		method2...10--------------->
//		method2...9--------------->
//		method2...8--------------->
//		method2...7--------------->
//		method2...6--------------->
//		method2...5--------------->
//		method2...4--------------->
//		method2...3--------------->
//		method2...2--------------->
//		method2...1--------------->
//		method2...0--------------->
//		method1...1
//		method1...2
//		method1...3
//		method1...4
//		method1...5
//		method1...6
//		method1...7
//		method1...8
//		method1...9
//		method1...10
//		method1...11
//		method1...12
//		method1...13
//		method1...14
//		method1...15
//		method1...16
//		method1...17
//		method1...18
//		method1...19
//		method1...20
	}
}
class TestJavaMonitor_T1{
	int k = 0;
	public synchronized void method1(){
		for(int i = 0;i<20;i++){
			k++;
			System.out.println("method1..."+k);
		}
	}
	
	public synchronized void method2(){
		for(int i = 0;i<20;i++){
			k--;
			System.out.println("method2..."+k+"--------------->");
		}
	}
	//沒有被監視的內容
	public void method3(){
		for(int i = 0;i<20;i++){
			k--;
			System.out.println("method3..."+k);
		}
	}
}



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