你真的懂synchronized鎖?

wenbochang發表於2023-04-14

1. 前言

synchronized在我們的程式中非常的常見,主要是為了解決多個執行緒搶佔同一個資源。那麼我們知道synchronized有多種用法,以下從實踐出發,題目由簡入深,看你能答對幾道題目?

2. 問題

呼叫程式碼如下

public static void main(String[] args) throws Exception {
	ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1024));

	SyncTest st = new SyncTest();
	// 注意是不同方法
	executor.submit(() -> st.sync1_1());
	executor.submit(() -> st.sync1_2());

	executor.shutdown();
}

問題2.1

鎖lock全域性物件,會輸出什麼?

public static final Object LOCK = new Object();

public void sync1_1() {
	// 鎖住lock物件
	synchronized (LOCK) {
		sleep("sync1_1");
	}
}

public void sync1_2() {
	// 鎖住lock物件
	synchronized (LOCK) {
		sleep("sync1_2");
	}
}

public static void sleep(String name) {
	try {
		log.info(name + " get lock");
		TimeUnit.SECONDS.sleep(1);
		log.info(name + " release lock");
	} catch (Exception e) {}
}

點選檢視答案(請思考後在點選檢視)
[INFO  2023-04-14 15:12:46.639] [pool-2-thread-1] [] - [SyncTest.java.sleep:86] [sync1_1 get lock]
[INFO  2023-04-14 15:12:47.652] [pool-2-thread-1] [] - [SyncTest.java.sleep:88] [sync1_1 release lock]

[INFO  2023-04-14 15:12:47.652] [pool-2-thread-2] [] - [SyncTest.java.sleep:86] [sync1_2 get lock]
[INFO  2023-04-14 15:12:48.652] [pool-2-thread-2] [] - [SyncTest.java.sleep:88] [sync1_2 release lock]

等待執行緒A執行完成後,執行緒B才能執行,否則阻塞。符合我們預期。鎖的資源就是我們所謂的LOCK物件

問題2.2

鎖this物件,會輸出什麼?this是代表什麼?

public void sync2_1() {
	synchronized (this) {
		sleep("sync2_1");
	}
}
public void sync2_2() {
	synchronized (this) {
		sleep("sync2_2");
	}
}

點選檢視答案(請思考後在點選檢視)
[INFO  2023-04-14 15:12:46.639] [pool-2-thread-1] [] - [SyncTest.java.sleep:86] [sync2_1 get lock]
[INFO  2023-04-14 15:12:47.652] [pool-2-thread-1] [] - [SyncTest.java.sleep:88] [sync2_1 release lock]

[INFO  2023-04-14 15:12:47.652] [pool-2-thread-2] [] - [SyncTest.java.sleep:86] [sync2_2 get lock]
[INFO  2023-04-14 15:12:48.652] [pool-2-thread-2] [] - [SyncTest.java.sleep:88] [sync2_2 release lock]

等待執行緒A執行完成後,執行緒B才能執行,否則阻塞。符合我們預期。鎖的是呼叫方,SyncTest st = new SyncTest(); 中的st物件。 st是SyncTest類的一個物件。也就是鎖的這個this資源

問題2.3

鎖方法,會輸出什麼? 鎖的又是什麼資源?

public synchronized void sync3_1() {
	sleep("sync3_1");
}

public synchronized void sync3_2() {
	sleep("sync3_2");
}

點選檢視答案(請思考後在點選檢視) 結論同 問題2.2

問題2.4

鎖static方法,會輸出什麼? 鎖的又是什麼資源?

public static synchronized void sync4_1() {
	sleep("sync4_1");
}

public static synchronized void sync4_2() {
	sleep("sync4_2");
}

點選檢視答案(請思考後在點選檢視)
[INFO  2023-04-14 15:12:46.639] [pool-2-thread-1] [] - [SyncTest.java.sleep:86] [sync4_1 get lock]
[INFO  2023-04-14 15:12:47.652] [pool-2-thread-1] [] - [SyncTest.java.sleep:88] [sync4_1 release lock]

[INFO  2023-04-14 15:12:47.652] [pool-2-thread-2] [] - [SyncTest.java.sleep:86] [sync4_2 get lock]
[INFO  2023-04-14 15:12:48.652] [pool-2-thread-2] [] - [SyncTest.java.sleep:88] [sync4_2 release lock]

等待執行緒A執行完成後,執行緒B才能執行,否則阻塞。符合我們預期。因為是在static上面加鎖,而static方法即是類方法,因此他鎖的是這個類,也就是對SyncTest這個this class加的鎖

問題2.5

鎖類的class,會輸出什麼? 鎖的又是什麼資源?

public void sync5_1() {
	synchronized (SyncTest.class) {
		sleep("sync5_1");
	}
}

public void sync5_2() {
	synchronized (SyncTest.class) {
		sleep("sync5_2");
	}
}

點選檢視答案(請思考後在點選檢視) 結論同問題2.4

3. 問題(修改呼叫方式)

其他全部程式碼不改變,僅修改呼叫方式。具體程式碼如下

public static void main(String[] args) throws Exception {
	ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1024));

	// 這個是new了一個st1
	SyncTest st1 = new SyncTest();
	executor.submit(() -> st1.sync1_1());
	
	// 這裡new了一個st2
	SyncTest st2 = new SyncTest();
	executor.submit(() -> st2.sync1_2());

	executor.shutdown();
}

其他全部不變,問題從2.1 - 2.5重新全部呼叫一遍。結果又是否相同。
注意一下:呼叫方為 new 了兩個st1以及st2, 如果你完全理解上述問題,這個問題就非常簡單了。我們以問題1以及問題2為例:

問題2.1

[INFO  2023-04-14 15:12:46.639] [pool-2-thread-1] [] - [SyncTest.java.sleep:86] [sync1_1 get lock]
[INFO  2023-04-14 15:12:47.652] [pool-2-thread-1] [] - [SyncTest.java.sleep:88] [sync1_1 release lock]

[INFO  2023-04-14 15:12:47.652] [pool-2-thread-2] [] - [SyncTest.java.sleep:86] [sync1_2 get lock]
[INFO  2023-04-14 15:12:48.652] [pool-2-thread-2] [] - [SyncTest.java.sleep:88] [sync1_2 release lock]

這是由於無論new幾個st,鎖的永遠是唯一資源lock,因此結論不變

問題2.2

[INFO  2023-04-14 15:26:31.676] [pool-2-thread-2] [] - [SyncTest.java.sleep:86] [sync2_1 get lock]
[INFO  2023-04-14 15:26:31.676] [pool-2-thread-1] [] - [SyncTest.java.sleep:86] [sync2_1 get lock]

[INFO  2023-04-14 15:26:32.688] [pool-2-thread-1] [] - [SyncTest.java.sleep:88] [sync2_1 release lock]
[INFO  2023-04-14 15:26:32.688] [pool-2-thread-2] [] - [SyncTest.java.sleep:88] [sync2_1 release lock]

這個結果就非常有意思了。注意看,執行緒B並沒有阻塞,而是直接獲取到了這個資源。也就是synchronized失效了。我們來分析一下為什麼synchronized失效了?
我們知道在問題2中,synchronized鎖的是this物件,而這個this物件分別為st1, 以及st2。那麼是不是就是兩個資源了。如果是兩個資源,就不存在互斥的作用了,也就是不會相互爭奪資源。

請各位讀者自己分析問題2.3 - 2.5的第二種程式碼呼叫方式的結果。

4. 結論

synchronized是我們工程中常用的一個方法。但對於其用法,如果深究,還是有非常多意外的驚喜。如果小夥伴有其他問題,隨時歡迎交流討論

相關文章