單例模式的五種實(shí)現(xiàn)看似喪心病狂,其實(shí)是一個由淺入深,再到化繁為簡的過程。
餓漢式
餓漢式的實(shí)現(xiàn)思路是初始化的過程中就加載完成單例,而不是延遲再加載,有一種饑不擇食的感覺,代碼如下。
import java.util.concurrent.atomic.AtomicLong;
public class IdGen {
private AtomicLong id = new AtomicLong(0);
private static final IdGen instance = new IdGen();
private IdGen() {
}
public static IdGen getInstance() {
return instance;
}
public long getId() {
var r = id.incrementAndGet();
System.out.println(r);
return r;
}
}
這就適合一些業(yè)務(wù)加載時間長或者必須提前加載的使用場景。不少人覺得業(yè)務(wù)加載時間長應(yīng)該延遲加載,這樣才能加快啟動速度。其實(shí)延遲加載會影響用戶體驗,因為延遲加載后,用戶請求發(fā)過來,服務(wù)器還沒加載完一直卡著響應(yīng),體驗極差。
注意看,Runtime類中的單例模式是用餓漢式實(shí)現(xiàn)的。
/**
* Every Java application has a single instance of class
* <code>Runtime</code> that allows the application to interface with
* the environment in which the application is running. The current
* runtime can be obtained from the <code>getRuntime</code> method.
* <p>
* An application cannot create its own instance of this class.
*
* @author unascribed
* @see java.lang.Runtime#getRuntime()
* @since JDK1.0
*/
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
/** Don't let anyone else instantiate this class */
private Runtime() {
}
// ....
public void addShutdownHook(Thread hook) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("shutdownHooks"));
}
ApplicationShutdownHooks.add(hook);
}
// ...
}
懶漢式
最原始的懶漢式單例給方法加鎖,實(shí)現(xiàn)延遲加載。但這種方法根本就不支持并發(fā),是個串行操作。如果你用這種方法實(shí)現(xiàn),一旦遇到并發(fā)的資源訪問,系統(tǒng)根本hold不住,背鍋概率99%。
import java.util.concurrent.atomic.AtomicLong;
public class IdGen {
private AtomicLong id = new AtomicLong(0);
private static IdGen instance;
private IdGen() {
}
public static synchronized IdGen getInstance() {
if (instance == null) {
instance = new IdGen();
}
return instance;
}
public long getId() {
var r = id.incrementAndGet();
return r;
}
}
雙重檢測
雙重檢測既實(shí)現(xiàn)了延遲加載,又支持了并發(fā)。不少人可能會疑惑,為什么要檢測兩次instance==null,原因其實(shí)很簡單,如果只檢測一次,假設(shè)兩個線程調(diào)用getInstance(),其中一個線程new出了單例,另一個線程等待第一個釋放鎖之后仍然可以new出單例,那這就破壞了單例模式,因此需要檢測兩次。
有的人可能會說創(chuàng)建對象singleton = new Singleton()的底層是分為三個步驟:
- 為對象分配內(nèi)存空間;
- 初始化對象;
- 對象地址的引用。
因此要給 instance 成員變量加上 volatile 關(guān)鍵字,禁止指令重排序才行。實(shí)際上,只有很低版本的 Java 才會有這個問題。我們現(xiàn)在用的高版本的 Java 已經(jīng)在 JDK 內(nèi)部實(shí)現(xiàn)中解決了這個問題(解決的方法很簡單,只要把對象 new 操作和初始化操作設(shè)計為原子操作,就自然能禁止重排序)。
import java.util.concurrent.atomic.AtomicLong;
public class IdGen {
private AtomicLong id = new AtomicLong(0);
private static IdGen instance;
private IdGen() {
}
public static IdGen getInstance() {
if (instance == null) {
synchronized (IdGen.class) {
if (instance == null) {
instance = new IdGen();
}
}
}
return instance;
}
public long getId() {
var r = id.incrementAndGet();
return r;
}
}
內(nèi)部靜態(tài)類
內(nèi)部靜態(tài)類的實(shí)現(xiàn)也實(shí)現(xiàn)了延遲加載,把并發(fā)的操作交給了JVM來處理,不過仍然有序列化和反射破壞單例模式的問題。
import java.util.concurrent.atomic.AtomicLong;
public class IdGen {
private AtomicLong id = new AtomicLong(0);
private IdGen() {
}
private static class SingletonHolder {
private static final IdGen instance = new IdGen();
}
public static IdGen getInstance() {
return SingletonHolder.instance;
}
public long getId() {
var r = id.incrementAndGet();
return r;
}
}
枚舉
枚舉也是實(shí)現(xiàn)了延遲加載,并發(fā)交由JVM處理,并且沒有序列化和破壞單例模式的問題,可以說是延遲加載中的最優(yōu)解。
import java.util.concurrent.atomic.AtomicLong;
// 測試類直接調(diào)用IdGen.INSTANCE
public enum IdGen {
INSTANCE;
private AtomicLong id = new AtomicLong(0);
public long getId() {
var r = id.incrementAndGet();
System.out.println(r);
return r;
}
}
總結(jié)
綜上,我們不難發(fā)現(xiàn)單例大體可以分為提前加載和延遲加載兩大類,餓漢式單例在指定的應(yīng)用場景是無可替代的,枚舉實(shí)現(xiàn)的單例簡潔安全,雙重檢測能考察你對鎖的理解,因此單例面試題深得面試官喜愛。