類初始化鎖
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
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步驟
- 給對象分配內存空間
- 對象初始化
- 將引用指向對象的內存空間
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併發編程的藝術》