單例設(shè)計(jì)模式理解起來(lái)非常簡(jiǎn)單。一個(gè)類只允許創(chuàng)建一個(gè)對(duì)象(或者實(shí)例),那這個(gè)類就是一個(gè)單例類,這種設(shè)計(jì)模式就叫單例模式。
使用場(chǎng)景
處理資源訪問(wèn)沖突
下面的示例中如果每個(gè)類都創(chuàng)建一個(gè) Logger 實(shí)例,就可能造成日志內(nèi)容被覆蓋的情況。
public class Logger {
private FileWriter writer;
public Logger() {
File file = new File("log.txt");
writer = new FileWriter(file, true); //true表示追加寫(xiě)入
}
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è)計(jì)為單例類。比如,配置信息類,全局 ID 生成器等。
如何實(shí)現(xiàn)一個(gè)單例?
要實(shí)現(xiàn)一個(gè)單例,我們要考慮以下幾點(diǎn):
- 構(gòu)造函數(shù)需要是 private 訪問(wèn)權(quán)限的,這樣才能避免外部通過(guò) new 創(chuàng)建實(shí)例;
- 考慮對(duì)象創(chuàng)建時(shí)的線程安全問(wèn)題;
- 考慮是否支持延遲加載;
- 考慮 getInstance() 性能是否高(是否加鎖)。
餓漢式
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
懶漢式
懶漢式相對(duì)于餓漢式的優(yōu)勢(shì)是支持延遲加載。但缺點(diǎn)也很明顯,因?yàn)槭褂昧?code>synchronized關(guān)鍵字導(dǎo)致這個(gè)方法的并發(fā)度很低。如果這個(gè)單例類偶爾會(huì)被用到,那這種實(shí)現(xiàn)方式還可以接受。但是,如果頻繁地用到,就會(huì)導(dǎo)致性能瓶頸,這種實(shí)現(xiàn)方式就不可取了。
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
雙重檢測(cè)
這是一種既支持延遲加載、又支持高并發(fā)的單例實(shí)現(xiàn)方式。
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) { // 此處為類級(jí)別的鎖
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
在 java1.5 以下instance = new Singleton();有指令重排問(wèn)題,需要給instance成員變量加上volatile關(guān)鍵字,java1.5 之后不會(huì)再這個(gè)有問(wèn)題。
靜態(tài)內(nèi)部類
這種方式利用了 Java 的靜態(tài)內(nèi)部類,有點(diǎn)類似餓漢式,但又能做到了延遲加載。
當(dāng)外部類 Singleton 被加載的時(shí)候,并不會(huì)創(chuàng)建 SingletonHolder 實(shí)例對(duì)象。只有當(dāng)調(diào)用 getInstance() 方法時(shí),SingletonHolder 才會(huì)被加載,這個(gè)時(shí)候才會(huì)創(chuàng)建 instance。insance 的唯一性、創(chuàng)建過(guò)程的線程安全性,都由 JVM 來(lái)保證。所以,這種實(shí)現(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;
}
}
枚舉
這是一種最簡(jiǎn)單的實(shí)現(xiàn)方式,基于枚舉類型的單例實(shí)現(xiàn)。這種實(shí)現(xiàn)方式通過(guò) Java 枚舉類型本身的特性,保證了實(shí)例創(chuàng)建的線程安全性和實(shí)例的唯一性。
public enum IdGenerator {
INSTANCE;
private AtomicLong id = new AtomicLong(0);
public long getId() {
return id.incrementAndGet();
}
}
如何實(shí)現(xiàn)線程唯一的單例?
上面的單例類對(duì)象是進(jìn)程唯一的,一個(gè)進(jìn)程只能有一個(gè)單例對(duì)象。那如何實(shí)現(xiàn)一個(gè)線程唯一的單例呢?
假設(shè) IdGenerator 是一個(gè)線程唯一的單例類。在線程 A 內(nèi),我們可以創(chuàng)建一個(gè)單例對(duì)象 a。因?yàn)榫€程內(nèi)唯一,在線程 A 內(nèi)就不能再創(chuàng)建新的 IdGenerator 對(duì)象了,而線程間可以不唯一,所以,在另外一個(gè)線程 B 內(nèi),我們還可以重新創(chuàng)建一個(gè)新的單例對(duì)象 b。
我們通過(guò)一個(gè) ConcurrentHashMap 來(lái)存儲(chǔ)對(duì)象,其中 key 是線程 ID,value 是對(duì)象。這樣我們就可以做到,不同的線程對(duì)應(yīng)不同的對(duì)象,同一個(gè)線程只能對(duì)應(yīng)一個(gè)對(duì)象。實(shí)際上,Java 語(yǔ)言本身提供了 ThreadLocal 工具類,可以更加輕松地實(shí)現(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();
}
}