單例設(shè)計模式理解起來非常簡單甘桑。一個類只允許創(chuàng)建一個對象(或者實例),那這個類就是一個單例類歹叮,這種設(shè)計模式就叫單例模式跑杭。
使用場景
處理資源訪問沖突
下面的示例中如果每個類都創(chuàng)建一個 Logger 實例,就可能造成日志內(nèi)容被覆蓋的情況咆耿。
public class Logger {
private FileWriter writer;
public Logger() {
File file = new File("log.txt");
writer = new FileWriter(file, true); //true表示追加寫入
}
public void log(String message) {
writer.write(mesasge);
}
}
public class UserController {
private Logger logger = new Logger();
public void login(String username, String password) {
// ...省略業(yè)務(wù)邏輯代碼...
logger.log(username + " logined!");
}
}
public class OrderController {
private Logger logger = new Logger();
public void create(OrderVo order) {
// ...省略業(yè)務(wù)邏輯代碼...
logger.log("Created an order: " + order.toString());
}
}
表示全局唯一類
如果有些數(shù)據(jù)在系統(tǒng)中只應(yīng)保存一份德谅,那就比較適合設(shè)計為單例類。比如萨螺,配置信息類窄做,全局 ID 生成器等愧驱。
如何實現(xiàn)一個單例?
要實現(xiàn)一個單例椭盏,我們要考慮以下幾點:
- 構(gòu)造函數(shù)需要是 private 訪問權(quán)限的组砚,這樣才能避免外部通過 new 創(chuàng)建實例;
- 考慮對象創(chuàng)建時的線程安全問題掏颊;
- 考慮是否支持延遲加載惫确;
- 考慮 getInstance() 性能是否高(是否加鎖)。
餓漢式
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
懶漢式
懶漢式相對于餓漢式的優(yōu)勢是支持延遲加載蚯舱。但缺點也很明顯改化,因為使用了synchronized
關(guān)鍵字導(dǎo)致這個方法的并發(fā)度很低。如果這個單例類偶爾會被用到枉昏,那這種實現(xiàn)方式還可以接受陈肛。但是,如果頻繁地用到兄裂,就會導(dǎo)致性能瓶頸句旱,這種實現(xiàn)方式就不可取了。
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
雙重檢測
這是一種既支持延遲加載晰奖、又支持高并發(fā)的單例實現(xiàn)方式谈撒。
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) { // 此處為類級別的鎖
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
在 java1.5 以下instance = new Singleton();
有指令重排問題,需要給instance
成員變量加上volatile
關(guān)鍵字匾南,java1.5 之后不會再這個有問題啃匿。
靜態(tài)內(nèi)部類
這種方式利用了 Java 的靜態(tài)內(nèi)部類,有點類似餓漢式蛆楞,但又能做到了延遲加載溯乒。
當(dāng)外部類 Singleton 被加載的時候,并不會創(chuàng)建 SingletonHolder 實例對象豹爹。只有當(dāng)調(diào)用 getInstance() 方法時裆悄,SingletonHolder 才會被加載,這個時候才會創(chuàng)建 instance臂聋。insance 的唯一性光稼、創(chuàng)建過程的線程安全性,都由 JVM 來保證孩等。所以艾君,這種實現(xiàn)方法既保證了線程安全,又能做到延遲加載瞎访。
public class Singleton {
private Singleton() {}
private static class SingletonHolder{
private static final Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}
枚舉
這是一種最簡單的實現(xiàn)方式腻贰,基于枚舉類型的單例實現(xiàn)。這種實現(xiàn)方式通過 Java 枚舉類型本身的特性扒秸,保證了實例創(chuàng)建的線程安全性和實例的唯一性播演。
public enum IdGenerator {
INSTANCE;
private AtomicLong id = new AtomicLong(0);
public long getId() {
return id.incrementAndGet();
}
}
如何實現(xiàn)線程唯一的單例冀瓦?
上面的單例類對象是進程唯一的,一個進程只能有一個單例對象写烤。那如何實現(xiàn)一個線程唯一的單例呢翼闽?
假設(shè) IdGenerator 是一個線程唯一的單例類。在線程 A 內(nèi)洲炊,我們可以創(chuàng)建一個單例對象 a感局。因為線程內(nèi)唯一,在線程 A 內(nèi)就不能再創(chuàng)建新的 IdGenerator 對象了暂衡,而線程間可以不唯一询微,所以,在另外一個線程 B 內(nèi)狂巢,我們還可以重新創(chuàng)建一個新的單例對象 b撑毛。
我們通過一個 ConcurrentHashMap 來存儲對象,其中 key 是線程 ID唧领,value 是對象藻雌。這樣我們就可以做到,不同的線程對應(yīng)不同的對象斩个,同一個線程只能對應(yīng)一個對象胯杭。實際上,Java 語言本身提供了 ThreadLocal 工具類受啥,可以更加輕松地實現(xiàn)線程唯一單例做个。
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private static final ConcurrentHashMap<Long, IdGenerator> instances
= new ConcurrentHashMap<>();
private IdGenerator() {}
public static IdGenerator getInstance() {
Long currentThreadId = Thread.currentThread().getId();
instances.putIfAbsent(currentThreadId, new IdGenerator());
return instances.get(currentThreadId);
}
public long getId() {
return id.incrementAndGet();
}
}