有一些類,在記憶體中沒有必要存在多個對象。這時候就出現了單例模式。
1. 餓漢式
使用
static
保證了線程安全,在類加載到記憶體的時候,進行執行個體化。
/**
* 餓漢式
* 類加載到記憶體後,就執行個體化一個單例,JVM保證線程安全
* 簡單實用,推薦使用!
* 唯一缺點:不管用到與否,類裝載時就完成執行個體化
* Class.forName("")
* (話說你不用的,你裝載它幹啥)
*/
public class Mgr01 {
private static final Mgr01 INSTANCE = new Mgr01();
private Mgr01() {};
public static Mgr01 getInstance() {
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
Mgr01 m1 = Mgr01.getInstance();
Mgr01 m2 = Mgr01.getInstance();
System.out.println(m1 == m2);
}
}
2. 懶漢式
有人說上面的餓漢式,我都還沒有用,你就給我建立了一個對象,能不能在我用的時候再建立對象?于是又有了懶漢式。在調用
getInstance
方法的時候,才去建立對象,而且建立之前先判斷是不是為空。
單線程環境下,這段代碼确實沒有問題。但是多線程情況下,就會有問題。看代碼中的注釋(很好了解的)。
/**
* lazy loading
* 也稱懶漢式
* 雖然達到了按需初始化的目的,但卻帶來線程不安全的問題
*/
public class Mgr03 {
private static Mgr03 INSTANCE;
private Mgr03() {
}
public static Mgr03 getInstance() {
if (INSTANCE == null) { // 一個線程過來了,判斷了,INSTANCE 是null。這時候又有一個線程過來了,
// 也判斷了INSTANCE 是null。然後第一個線程繼續執行,建立了一個對象。接着第二個線程繼續開始執行,
//也會建立一個新的對象(第二個線程已經執行過判斷INSTANCE 是不是null的操作)。這時候就不能保證單例了。
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr03();
}
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->
System.out.println(Mgr03.getInstance().hashCode())
).start();
}
}
}
3. 如何解決懶漢式中存在的線程安全問題?
-
getInstance
方法上加個鎖不就行了
确實能夠達到目的,但是又有人說了,整個函數加鎖,有效率問題。能不能将鎖細化?于是又有了第
種方案。2
/**
* lazy loading
* 也稱懶漢式
* 雖然達到了按需初始化的目的,但卻帶來線程不安全的問題
* 可以通過synchronized解決,但也帶來效率下降
*/
public class Mgr04 {
private static Mgr04 INSTANCE;
private Mgr04() {
}
public static synchronized Mgr04 getInstance() {
if (INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr04();
}
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr04.getInstance().hashCode());
}).start();
}
}
}
- 加鎖之前,先判斷
是不是instance
,是null
然後才加鎖。null
乍一看,好像挺好的,沒啥問題。但是這種寫法也是有線程安全問題的(參考方法中的注釋)。
/**
* lazy loading
*/
public class Mgr05 {
private static Mgr05 INSTANCE;
private Mgr05() {
}
public static Mgr05 getInstance() {
if (INSTANCE == null) { // 一個線程是來了,判斷INSTANCE 是null,這時候第二個線程來了,
//也判斷了INSTANCE 是null。線程二獲得了鎖,然後建立了對象,執行完釋放了鎖;第二個線程獲得鎖,
//繼續執行,也會建立一個新的對象(因為它沒有再次判斷INSTANCE 是不是null)。這不就有兩個對象了嗎?
//妄圖通過減小同步代碼塊的方式提高效率,然後不可行
synchronized (Mgr05.class) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr05();
}
}
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr05.getInstance().hashCode());
}).start();
}
}
}
- 加鎖之後,再判斷
是不是INSTANCE
上面的代碼是由于加鎖之後,沒有判斷null
是不是INSTANCE
導緻的。那簡單,加鎖之後再判斷一下null
是不是INSTANCE
null
不就解決了嗎?
這就引出了單例模式
的寫法。也就是Double Check Lock
方法裡面,加鎖之前和之後分别檢查下getInstance
是不是INSTANCE
null
。
沒問題了嗎?注意代碼中的
volatile
是注釋掉的。
這就是一個面試題了。單例模式中的
寫法,執行個體變量是否需要加DCL
以及為什麼?volatile
/**
* lazy loading
*/
public class Mgr06 {
private static /*volatile*/ Mgr06 INSTANCE; //JIT
private Mgr06() {
}
public static Mgr06 getInstance() {
if (INSTANCE == null) {
//雙重檢查
synchronized (Mgr06.class) {
if(INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr06();
}
}
}
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for(int i=0; i<100; i++) {
new Thread(()->{
System.out.println(Mgr06.getInstance().hashCode());
}).start();
}
}
}
4. new
對象的操作中的指令重排序問題
new
要回答單例模式中的
DCL
寫法,執行個體變量是否需要加
volatile
以及為什麼?這個問題,首先得要清楚
new
一個對象的操作其實是分為三條指令的。
分别是申請記憶體賦預設值、初始化、将執行個體變量指向對象。
這三條指令是可能發生指令重排序的,初始化操作和将将執行個體變量指向對象的操作的順序會互換。這時候就會出現線程安全的問題。
第一個線程來了,判斷
INSTANCE
是
null
,然後加鎖,進入了
new
對象的過程,如果發生指令重排序(先對執行個體變量進行了指派操作),在
new
到一半的時候,
INSTANCE
已經被指派,這時候,第二個線程來了,判斷
INSTANCE
不是
null
,會直接傳回還沒有初始化完成的
INSTANCE
對象,就會出現問題。
加
volatile
禁止指令重排序。就可以解決上述問題。
5. 推薦使用餓漢式
其實工作中使用餓漢式就夠了,沒必要搞得這麼複雜。所謂面試造火箭,工作擰螺絲。。。。内卷的厲害。