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步驟
- 給物件分配記憶體空間
- 物件初始化
- 將引用指向物件的記憶體空間
對於執行緒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步驟
- 給物件分配記憶體空間
- 物件初始化
- 將引用指向物件的記憶體空間
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併發程式設計的藝術》