定義:
保證一個(gè)類僅有一個(gè)實(shí)例, 并提供一個(gè)全局訪問(wèn)點(diǎn)
類型:
創(chuàng)建型
使用場(chǎng)景
- 確保任何情況下都絕對(duì)只有一個(gè)實(shí)例
coding
單例模式需要注意的點(diǎn)
- 私有構(gòu)造器
- 線程安全
- 延遲加載
- 序列化和反序列化安全
- 防止反射機(jī)制破壞單例模式
單例模式的N種寫法
1. 餓漢式
- 實(shí)現(xiàn)簡(jiǎn)單
- 線程安全
public class HungrySingleton {
private static HungrySingleton INSTANCE = new HungrySingleton();
private HungrySingleton(){}
public static HungrySingleton getInstance() {
return INSTANCE;
}
}
初始化類時(shí)就加載, 如果不使用就會(huì)浪費(fèi)內(nèi)存
2. 懶漢式
- 實(shí)現(xiàn)簡(jiǎn)單
- 延遲加載
public class LazySingleton {
private static LazySingleton INSTANCE;
private LazySingleton(){}
public static LazySingleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new LazySingleton();
}
return INSTANCE;
}
}
懶漢式的優(yōu)點(diǎn)是延遲加載,等到需要的時(shí)候才會(huì)創(chuàng)建實(shí)例, 但他是線程不安全的, 當(dāng)兩個(gè)線程同時(shí)進(jìn)入getInstance方法時(shí), 線程1和2都執(zhí)行到
INSTANCE == null, 此時(shí)INSTANCE如果還未創(chuàng)建, 將會(huì)創(chuàng)建兩個(gè)實(shí)例線程不安全可以通過(guò)多線程調(diào)試來(lái)復(fù)現(xiàn)
IDEA多線程調(diào)試
3. 懶漢式 + 同步鎖
- 延遲加載
- 線程安全
public class LazySyncSingleton {
private static LazySyncSingleton INSTANCE;
private LazySyncSingleton(){}
public static synchronized LazySyncSingleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new LazySyncSingleton();
}
return INSTANCE;
}
}
這樣子線程就安全了,但是消耗了不必要的同步資源彬呻,不推薦這樣使用挡育。
4. DCL模式(Double CheckLock) - 雙重檢查
- 延遲加載
- 線程安全
- 相對(duì)懶漢式 + 同步鎖的方式只在初始化時(shí)才會(huì)加鎖, 提高了效率
public class LazyDoubleCheckSingleton {
private static LazyDoubleCheckSingleton INSTANCE;
private LazyDoubleCheckSingleton(){}
public static synchronized LazyDoubleCheckSingleton getInstance() {
if (INSTANCE == null) {
synchronized (LazyDoubleCheckSingleton.class) {
if (INSTANCE == null) {
INSTANCE = new LazyDoubleCheckSingleton();
}
}
}
return INSTANCE;
}
}
通過(guò)兩個(gè)判斷讲仰,第一層是避免不必要的同步楞慈,第二層判斷是否為null膊存。
可能會(huì)出現(xiàn)DCL模式失效的情況赘淮。
DCL模式失效:
singleton=new Singleton()
這句話執(zhí)行的時(shí)候岔帽,會(huì)進(jìn)行下列三個(gè)過(guò)程:
- 分配內(nèi)存。
- 初始化構(gòu)造函數(shù)和成員變量退渗。
- 將對(duì)象指向分配的空間脆炎。
由于JMM(Java Memory Model)的規(guī)定,可能會(huì)對(duì)單線程情況下不影響程序運(yùn)行結(jié)果的指令進(jìn)行重排序, 因此可能會(huì)出現(xiàn)1-2-3和1-3-2兩種情況氓辣。
所以,就會(huì)出現(xiàn)線程A進(jìn)行到1-3時(shí)袱蚓,就被線程B取走钞啸,此時(shí)B線程拿到的是一個(gè)還未初始化完成的對(duì)象, 這時(shí)就出現(xiàn)了異常, DCL模式就失效了。
可以使用 volatile 來(lái)解決重排序問(wèn)題
volatile 有禁止指令重排序的功能. volatile詳解
private volatile static LazyDoubleCheckSingleton INSTANCE;
5.內(nèi)部類實(shí)現(xiàn)單例
- 線程安全
- 實(shí)現(xiàn)簡(jiǎn)單
- 延遲加載
public class StaticInnerClassSingleton {
private static class SingletonHolder {
private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
}
private StaticInnerClassSingleton(){}
public static StaticInnerClassSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
利用了class的初始化鎖保證只有一個(gè)線程能加載內(nèi)部類
只有通過(guò)顯式調(diào)用 getInstance 方法時(shí)喇潘,才會(huì)顯式裝載 SingletonHolder 類体斩,從而實(shí)例化 instance (只有拿到初始化鎖的線程才會(huì)初始化對(duì)象)
6.枚舉
- 實(shí)現(xiàn)簡(jiǎn)單
- 線程安全
- 避免反序列化破壞單例
- 避免反射攻擊
public enum EnumSingleton {
INSTANCE(new Object());
EnumSingleton(Object data) {
this.data = data;
}
/**
* 單例實(shí)體
*/
private Object data;
public Object getData() {
return data;
}
public static EnumSingleton getInstance(){
return INSTANCE;
}
}
Spring管理單例bean就是容器管理
7.容器
- 統(tǒng)一管理單例
public class ContainerSingleton {
private ContainerSingleton(){}
private static Map<String,Object> CONTAINER = new HashMap<String,Object>();
public static void putInstance(String key,Object instance){
if(StringUtils.isNotBlank(key) && instance != null){
if(!CONTAINER.containsKey(key)){
CONTAINER.put(key,instance);
}
}
}
public static Object getInstance(String key){
return CONTAINER.get(key);
}
}
8.特殊的單例模式 ThreadLocal 實(shí)現(xiàn)線程單例
public class ThreadLocalInstance {
private static final ThreadLocal<ThreadLocalInstance> INSTANCE
= ThreadLocal.withInitial(ThreadLocalInstance::new);
private ThreadLocalInstance(){
System.out.println("init");
}
public static ThreadLocalInstance getInstance(){
return INSTANCE.get();
}
}
測(cè)試
public class ThreadLocalInstance {
private static final ThreadLocal<ThreadLocalInstance> INSTANCE
= ThreadLocal.withInitial(ThreadLocalInstance::new);
private ThreadLocalInstance(){
System.out.println("init");
}
public static ThreadLocalInstance getInstance(){
return INSTANCE.get();
}
}
運(yùn)行, 在輸出結(jié)果中可以看到, 用一個(gè)線程獲取到的實(shí)例都是相同的, 即每個(gè)線程中只有一個(gè)實(shí)例存在, 在很多情況下是非常有用的,篇幅原因就不詳細(xì)展開了,想詳細(xì)了解的可以看一下這邊文章 => Java并發(fā)編程:深入剖析ThreadLocal
源碼中的單例
單例在源碼中是廣泛使用的
比如常用的工具類 java.lang.Math#random
方法
public static double random() {
return RandomNumberGeneratorHolder.randomNumberGenerator.nextDouble();
}
這個(gè)RandomNumberGeneratorHolder.randomNumberGenerator
是什么呢?
private static final class RandomNumberGeneratorHolder {
static final Random randomNumberGenerator = new Random();
}
這正是上面提到的內(nèi)部類實(shí)現(xiàn)單例的模式.
其他的比如java.lang.Runtime
中
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
一個(gè)非常明顯的餓漢式單例 等
序列化對(duì)單例模式的破壞
java中提供了對(duì)象的序列化與反序列化功能, 對(duì)象實(shí)現(xiàn)了Serializable接口之后就可以對(duì)對(duì)象的實(shí)例進(jìn)行序列化與反序列化, 下面以HungrySingleton 為例看一下 反序列化破壞單例模式的實(shí)例
public class SerializationBrokenSingletonTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
HungrySingleton instance = HungrySingleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
// 序列化
oos.writeObject(instance);
File file = new File("singleton_file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
// 反序列化
HungrySingleton newInstance = (HungrySingleton) ois.readObject();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}
運(yùn)行結(jié)果:
com.hhx.design.pattern.creational.singleton.HungrySingleton@135fbaa4
com.hhx.design.pattern.creational.singleton.HungrySingleton@568db2f2
false
明顯看到 反序列化之后, 得到了一個(gè)不同的對(duì)象實(shí)例.
在HungrySingleton中添加readResolve()方法
private Object readResolve(){
return hungrySingleton;
}
再次運(yùn)行代碼
com.hhx.design.pattern.creational.singleton.HungrySingleton@135fbaa4
com.hhx.design.pattern.creational.singleton.HungrySingleton@135fbaa4
true
神奇的發(fā)現(xiàn)返回true, 這是怎么回事呢,
有興趣的朋友可以debug跟蹤一下
ObjectInputStream#readObject
ObjectInputStream#readObject0
ObjectInputStream#readOrdinaryObject
重點(diǎn)關(guān)注
ObjectInputStream#readOrdinaryObject
中的
obj = desc.isInstantiable() ? desc.newInstance() : null
與
Object rep = desc.invokeReadResolve(obj)
就可以知道 readResolve 的調(diào)用原理了.
反射對(duì)單例模式的破壞
public class ReflectBrokenSingletonTest {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
HungrySingleton instance = HungrySingleton.getInstance();
Class<HungrySingleton> clazz = HungrySingleton.class;
Constructor<HungrySingleton> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
HungrySingleton newInstance = constructor.newInstance();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}
- 對(duì)于在類加載階段就初始化的實(shí)例的單例模式(餓漢式, 內(nèi)部類)可以通過(guò)在構(gòu)造器中拋出異常的方式防止反射攻擊
private HungrySingleton(){
if(INSTANCE != null){
throw new RuntimeException("單例構(gòu)造器禁止反射調(diào)用");
}
}
對(duì)于懶加載的單例模式(懶漢式, 懶漢式+同步鎖, DCL模式), 如果在構(gòu)造器中拋出異常的話, 當(dāng)實(shí)例在反射調(diào)用constructor.newInstance()執(zhí)行之前就已經(jīng)實(shí)例化時(shí), 是可以按照預(yù)期拋出異常的, 但是如果單例模式中的實(shí)例還未被實(shí)例化, 執(zhí)行constructor.newInstance()不會(huì)拋出異常, 因?yàn)榇藭r(shí)INSTANCE == null.
優(yōu)點(diǎn):
- 內(nèi)存只有一個(gè)實(shí)例, 減少內(nèi)存開銷
- 設(shè)置全局訪問(wèn)點(diǎn), 嚴(yán)格控制訪問(wèn)
缺點(diǎn):
- 沒有接口, 擴(kuò)展困難