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
cn.bing.singleton.SingleTon@24e59eb1
cn.bing.singleton.SingleTon@52826699

餓漢式

爲什麼線程安全

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

/**
 * 餓漢式
 * @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併發編程的藝術》

 

 


 

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