1. 程式人生 > >java多執行緒學習(十一) 常見的單例模式執行緒安全性分析

java多執行緒學習(十一) 常見的單例模式執行緒安全性分析

類初始化鎖

 怎麼理解?

為什麼需要了解?

常見的單例模式分析

懶漢式

為什麼執行緒不安全

驗證

餓漢式

為什麼執行緒安全

雙重檢查鎖定方式

演變由來

為什麼執行緒不安全

如何解決執行緒不安全

靜態類方式

為什麼執行緒安全

結論


類初始化鎖

Java語言規範規定,對於每一個類或介面C,都有一個唯一的初始化鎖LC與之對應,從C到LC的對映,由JVM的具體實現去自由實現。JVM在初始化期間會獲取這個初始化鎖,並且每個執行緒至少獲取一次鎖來確保這個類被初始化了。
這個過程比較冗長,這裡不做過多描述,總之就是JVM通過初始化鎖同步了多個執行緒同時初始化一個物件的操作,保證類不會被多次初始化。

 怎麼理解?

執行緒A 、執行緒B 同時去訪問類的屬性或者方法,兩個執行緒都會去獲取類初始化鎖

1. 假設執行緒A先獲取到鎖,此時執行緒B阻塞等待。

2. 執行緒A獲取到鎖 -----> 對類初始化(初始化靜態屬性),設定state = initialized   ----->  釋放鎖

3. 執行緒B獲取到鎖,讀取到state = initialized,得知類已經初始化了,釋放鎖

根據happen-before原則,執行緒A的釋放鎖happen-before執行緒B獲取鎖,這樣執行緒A對類的初始化,執行緒B是可見的

結論就是,當類已經被初始化了,其他執行緒能夠可見類的靜態屬性的值,但是如果一個執行緒在初始化之後,比如呼叫類的靜態方法(靜態方法沒有做同步控制)改變類的屬性的值,對其他執行緒不一定可見。

為什麼需要了解?

單例模式,實際上都是多個執行緒通過靜態方法訪問一個類的靜態變數

常見的場景是:

首次呼叫一個類的靜態方法的過程,首先進行的是獲取類鎖、初始化、釋放類鎖,在呼叫類的靜態方法。

如果一個類不是被首次訪問,當前執行緒也會去獲取類鎖,讀取到state = initialized 、釋放鎖,在呼叫類的靜態方法。

靜態方法對靜態變數的修改執行緒之間不具有可見性,不是立即可見的。

常見的單例模式分析

懶漢式

public class SingleTon {
	private SingleTon() {}
	private static SingleTon instance;
	public static SingleTon getInstance() {
		if(instance==null) {
			instance = new SingleTon();
		}
		return instance;
	}
}

為什麼執行緒不安全

多個執行緒訪問,類只會被初始化一次,假設存線上程A和執行緒B呼叫getInstance方法,呼叫方法前類已經初始化,此時instance為null,對於兩個執行緒而言,instance都是null。

執行緒A 執行到 instance ==null時候,向下執行,建立物件

物件的建立分為3步驟

  1. 給物件分配記憶體空間
  2. 物件初始化
  3. 將引用指向物件的記憶體空間

 

對於執行緒B而言,不會等待執行緒A物件建立完成,也會建立物件,這樣就可能存在建立多個物件的可能性。

驗證

為了模擬物件建立耗時的過程,在建構函式裡面sleep 一段時間。

package cn.bing.singleton;

import java.util.Date;

/**
 * 懶漢式
 * @author Administrator
 *
 */
public class SingleTon {
	private SingleTon() {
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	private static SingleTon instance;
	public static SingleTon getInstance() {
		System.out.println(Thread.currentThread().getName()+" enter : "+System.currentTimeMillis());
		if(instance==null) {
			System.out.println(Thread.currentThread().getName()+" contruct : "+System.currentTimeMillis());
			instance = new SingleTon();
		}
		return instance;
	}
	public static void main(String[] args) {
		Runnable run = new Runnable() {
			@Override
			public void run() {
				System.out.println(SingleTon.getInstance());
			}
		};
		System.out.println("current time: "+System.currentTimeMillis());
			Thread t1 = new Thread(run, "執行緒A");
			t1.start();
			Thread t2 = new Thread(run,"執行緒B");
			t2.start();
	}
}

執行結果:

current time: 1541646424662
執行緒B enter : 1541646424663
執行緒B contruct : 1541646424663
執行緒A enter : 1541646424663
執行緒A contruct : 1541646424663
[email protected]
[email protected]

餓漢式

為什麼執行緒安全

懶漢式是因為類初始化的時候,沒有對例項初始化,出現執行緒安全問題,那麼類初始化的時候就建立物件(上面說過初始化靜態屬性的過程對於其他執行緒而言是可見的),就可以保證物件的一致性了,這就是餓漢式.

/**
 * 餓漢式
 * @author Administrator
 *
 */
public class SingleTonHungry {
	private static SingleTonHungry instance = new SingleTonHungry();
	private SingleTonHungry() {
	}
	public static SingleTonHungry getInstance() {
		return SingleTonHungry.instance;
	}
}

雙重檢查鎖定方式

演變由來

懶漢式不安全的原因,靜態方法被多個執行緒同時訪問,只要只能一個執行緒去構建物件,其他執行緒只能阻塞,等到另一個執行緒釋放鎖了,也就是這個物件建立好了,再獲取這個物件,便執行緒安全 了,於是在靜態方法上加上類鎖synchronize

public class SingleTonLazySynchronize {
	private static SingleTonLazySynchronize instance;
	private SingleTonLazySynchronize() {}
	public static synchronized SingleTonLazySynchronize getInstance() {
		if(instance == null) {
			instance = new SingleTonLazySynchronize();
		}
		return instance;
	}
}

優點:執行緒安全

缺點: 一個執行緒等待另一個執行緒執行完畢,在多執行緒環境下,效率很低

那麼,是否只要控制物件的建立在同步程式碼塊中的話,是不是就行了呢?

為什麼執行緒不安全

package cn.bing.singleton;
/**
 * 雙重檢查鎖定延遲載入
 * @author Administrator
 *
 */
public class SingleTonDoubleLock {
	private static volatile SingleTonDoubleLock instance;
	private SingleTonDoubleLock() {}
	public static SingleTonDoubleLock getInstance() {
		if(instance==null) {//1
			synchronized (SingleTonDoubleLock.class) {//2
				if(instance==null)
				instance = new SingleTonDoubleLock();
			}
		}
		return instance;
	}
}

假設執行緒A和執行緒B呼叫getInstance方法,執行緒A獲取到類鎖,進入2,建立物件

物件的建立分為3步驟

  1. 給物件分配記憶體空間
  2. 物件初始化
  3. 將引用指向物件的記憶體空間

jvm可能對上面的指定進行重排序,可能是1,3,2的順序

此時,執行緒B執行到1,看到instance的地址不為空(由於重排序,可能物件還沒有初始化),直接就返回了地址,但是此時物件還沒有被初始化。

如何解決執行緒不安全

第一種方式,jvm禁止重排序

禁止物件建立過程中2,3的重排序,只要將instance申明為volatile型別.

package cn.bing.singleton;
/**
 * 雙重檢查鎖定延遲載入
 * @author Administrator
 *
 */
public class SingleTonDoubleLock {
	private static volatile SingleTonDoubleLock instance;
	private SingleTonDoubleLock() {}
	public static SingleTonDoubleLock getInstance() {
		if(instance==null) {//1
			synchronized (SingleTonDoubleLock.class) {//2
				instance = new SingleTonDoubleLock();
			}
		}
		return instance;
	}
}

第二種方式,只要另一個執行緒看不到重排序(靜態類解決方案)

靜態類方式

package cn.bing.singleton;

public class SingleTonStaticClass {
	private SingleTonStaticClass() {}
	static class InstanceHolder{
		private static SingleTonStaticClass instance = new SingleTonStaticClass(); 
	}
	public static SingleTonStaticClass getInstance() {
		return InstanceHolder.instance;
	}
}

為什麼執行緒安全

  假設存在兩個執行緒A,B ,此時SingleTonStaticClass沒有初始化

1. 執行緒A先獲取到外部類的初始化鎖,執行緒B只能等待。

2. 執行緒A執行類的初始化完畢,將state設定為initialized,釋放外部類的鎖,呼叫getInstance方法,獲取內部類的鎖

3. 執行緒B獲取到外部類的鎖,嘗試呼叫getInstance方法,因為沒有獲取到內部類的鎖,只能等待

4. 執行緒A完成內部類的初始化,釋放內部類的鎖,執行緒B拿到內部類的鎖,因為內部類已經初始化了,不會繼續初始化,直接釋放鎖。

這個過程中,執行緒B是看不到執行緒A對內部類的物件的重排序的。

根據happen-before原則, 執行緒A對內部類的靜態屬性初始化後的的值對執行緒B是可見的。

執行緒A,B在這個過程中是獲取兩次初始化鎖,後續執行緒C呼叫getInstance只會獲取一次外部類鎖,讀取到外部類state=initialized,釋放鎖。

-- 來自方騰飛《JAVA併發程式設計的藝術》

個人感覺和執行緒A、執行緒B一樣還是要獲取兩次類鎖

結論

考慮到延遲載入,執行緒安全的單例模式,選擇基於volatile的雙重檢查方式或者基於靜態類的方式建立。

參考:方騰飛《JAVA併發程式設計的藝術》