Java中synchronized實現類鎖的兩種方式及原理解析

簡介

上一篇文章《Java中synchronized實現對象鎖的兩種方式及原理解析》中,介紹了方法鎖的兩種實現方式及Synchronized的底層原理,本文將講解synchronized的類鎖的兩種實現方式。

一.類鎖的定義

什麼是類鎖

類鎖指synchronize修飾的靜態方法或指定鎖爲class對象。

類鎖來自何處?

不同的線程,訪問使用類鎖的方法的時候,他們獲取到的“鎖”,其實是Class對象。因爲同一個類中有且只有一個Class對象,但同一個類中可以有很多個其他對象。此時,就出現了同一個類中多個對象對Class對象使用的競爭,類鎖則保證了在同一時間,只允許一個線程訪問被類鎖鎖住的方法。

類鎖有兩種實現方式:

  1. synchronized加在static方法上(靜態方法鎖)。
  2. synchronized(*.class)代碼塊。

接下來我們分別來看一下類鎖的兩種實現形式,以及它們之間的區別。

二.靜態方法鎖的方式實現類鎖

我們使用類鎖,應該保證在同一個類中,不同對象的線程,執行同一方法時,保證多個線程串行執行此方法。則說明我們的類鎖生效了,帶着這個預期我們來看下,通過靜態方法鎖實現多個線程串行執行目標方法。

完整的代碼實例:

public class ClassLock1 implements Runnable {

	// 這兩個實例,聲明爲static,是因爲要在main方法中測試,與方法鎖無關
	static ClassLock1 instance1 = new ClassLock1();
	static ClassLock1 instance2 = new ClassLock1();

	// 關鍵: static synchronized兩個關鍵字同時使用
	private static synchronized void method() {
		System.out.println("線程名:" + Thread.currentThread().getName() + ",運行開始");
		try {
			Thread.sleep(4000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("線程:" + Thread.currentThread().getName() + ",運行結束");
	}

	@Override
	public void run() {
		method();
	}

	public static void main(String[] args) {
		Thread thread1 = new Thread(instance1);
		Thread thread2 = new Thread(instance2);
		thread1.start();
		thread2.start();
		try {
			//兩個線程必須都執行完畢後
			thread1.join();
			thread2.join();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("兩個線程都已經執行完畢");
	}
}

運行結果:

線程名:Thread-1,運行開始
線程:Thread-1,運行結束
線程名:Thread-0,運行開始
線程:Thread-0,運行結束
兩個線程都已經執行完畢

二.靜態方法鎖的實現原理:

當在靜態方法上,使用synchronized關鍵字修飾時,就可以是想方法的同步了。但其實使用類鎖的第二個關鍵點在,不同實例的線程,調用類中的方法時,也就是上述代碼中,main方法中的兩個線程thread1thread2分別對應的instance1instance2。這個代碼的精髓在於,同一個類中不同的兩個對象(instance1instance2)的兩個線程,要控制這兩個線程串行執行,則需要使用類鎖。那大家可能有下面的疑問:爲什麼要用類鎖,用對象鎖會怎樣?

爲什麼要用類鎖,用對象鎖會怎樣?

先解釋如果不加static,類鎖會變成什麼?如果看過前一篇文章《Java中synchronized實現對象鎖的兩種方式及原理解析》,你會知道:

synchronized能夠保證在同一時刻最多隻有一個線程執行該段代碼,以保證併發安全。

但只用的synchronized修飾的方法其實是方法鎖,也就是對象鎖,對象鎖的作用範圍僅限於同一個對象內。而上述代碼中,我們有兩個不同的對象(instance1instance2),所以如果你把static關鍵字去掉,則此同步方法就成了不同對象中的方法鎖,又因爲方法鎖是對象鎖中的一種實現形式,即此時instance1instance2分別持有的是一把對象鎖。對象鎖的特性是,每個對象的對象鎖只能鎖住自己對象內的線程,而無法鎖住其他的對象中的線程,這就是爲何對象鎖不能鎖住不同對象中的線程的原因,這也是對象鎖和類鎖的區別。

方法鎖的作用範圍是,同一個對象內的不同線程,而類鎖則可以控制不同對象中的不同線程。

爲什麼同步方法上一定要加上static才能實現類鎖?

同步方法加上static關鍵字後,那此方法在class創建的時候,就已經初始化好了。類中所有的實例,同步使用這個方法,鎖的作用範圍是最大的。

如果不加static關鍵字,則該方法會被instance1instance2同時訪問到,因爲鎖的範圍僅僅侷限在instance1instance2對象中。此時是不能實現instance1instance2兩個對象所在的不同線程之間的同步。

三.synchronized(*.class)代碼塊方式實現類鎖

類鎖的第二種實現方式,它的語義是:

只要同步代碼塊中是*.class,不管有多少個對象調用這段代碼,都表示大家共用一個對象,這就實現了不同的實例,串行的執行此代碼塊。

完整代碼示例:

public class ClassLock2 implements Runnable {

	static ClassLock2 instance1 = new ClassLock2();
	static ClassLock2 instance2 = new ClassLock2();

	@Override
	public void run() {
		method();
	}

	private void method() {
		//關鍵:用*.class作爲鎖對象,即使在不同的實例中,也能保證線程安全
		//對比:此形式類鎖,與對象鎖中的同步代碼塊鎖,只有鎖對象不同,同步代碼塊鎖中的鎖對象是this,而類鎖是*.class)
		synchronized (ClassLock2.class) {
			System.out.println("線程名:" + Thread.currentThread().getName() + ",運行開始");
			try {
				Thread.sleep(4000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println("線程:" + Thread.currentThread().getName() + ",運行結束");
		}
	}

	public static void main(String[] args) {
		Thread thread1 = new Thread(instance1);
		Thread thread2 = new Thread(instance2);
		thread1.start();
		thread2.start();
		try {
			//兩個線程必須都執行完畢後
			thread1.join();
			thread2.join();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("兩個線程都已經執行完畢");
	}

}

運行結果

線程名:Thread-0,運行開始
線程:Thread-0,運行結束
線程名:Thread-1,運行開始
線程:Thread-1,運行結束
兩個線程都已經執行完畢

可以看出,兩個線程依然是串行執行,此種方式和上一種方式的結果一致。

爲什麼Java的synchronized實現類鎖或對象鎖提供了兩種方式?

這兩種方式是:使用synchronized修飾方法或使用synchronized代碼塊。

提供兩種方式的原因是因爲synchronized關鍵字的缺陷之一就是效率低。

synchronized只有當同步方法執行完或執行時拋出異常或調用wait系列方法時,纔會釋放鎖。若不釋放鎖,其他線程只能等待,所以相對相率比較低。

做任何事,提高效率的第一性原理是:

在滿足要求的情況下,儘可能的少做事

所以synchronized關鍵字除了可以加在方法上之外,還提供了同步代碼塊,可以讓其在方法內部使用。我們在程序編寫時,應該在滿足要求的情況下,儘可能的減少synchronized代碼塊內的代碼量,因爲synchronized代碼塊內的代碼,都是串行執行的,效率自然不如多個線程並行處理效率高,所以應該將那些不需要同步的代碼移動到synchronized代碼塊外執行,這樣的效率會更高。這就是synchronized爲什麼提供兩種方式做同步。

四.總結

本文承接上文《Java中synchronized實現對象鎖的兩種方式及原理解析》講解完對象鎖後,又講解了類鎖。Java中的synchronized實現的對象鎖和類鎖的各種實現方式已經交代清楚,以及其實現原理也做了初步的解析。

但這些只是基礎部分,接下來的文章中,將重點分析7中不同情況下的synchronized是否能夠保證線程安全,徹底理解synchronized關鍵字的作用域,這樣才能不再使用同步的過程中,給自己挖坑,並且後續文章中,也是面試官常常問到場景,絕對不容錯過。喜歡本文,請點贊和收藏。

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