23种设计模式之单例模式
参考资料
- Java设计模式:23种设计模式全面解析(超级详细)
- 韩顺平老师的Java设计模式(图解+框架源码剖析)
- 秦小波老师的《设计模式之禅》
下文如有错漏之处,敬请指正
一、简介
定义
一个类只存在一个实例对象,并且自行实例化并向全局提供一个获取该实例的方法。
特点
- 单例模式是一种创建型模式
- 构造方法私有化,提供一个static方法用来获取实例
优点
-
由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,且创建或销毁时性能又无法优化,单例模式的优势就非常明显。
-
单例模式可以避免对资源的多重占用,例如一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作。
缺点
- 单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。
- 单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,而不关心它是否是单例的,是不是要单例取决于环境,单例模式把“要单例”和业务逻辑融合在一个类中。
通用类图
使用场景
-
任务管理器
-
回收站
-
数据库连接池
-
Spring的Bean
二、实现方式
- 饿汉式 :在类加载时创建(线程安全)
- 懒汉式:在调用时创建(非线程安全)
- DCL(线程安全)
- 静态内部类(线程安全)
- 枚举(推荐使用,保证线程安全、不支持克隆、反射和反序列化操作)
饿汉式
不管是否使用该对象,JVM一启动就创建之。
package singleTon;
import java.lang.reflect.Constructor;
public class HungrySingleTon {
private String slogan;
private HungrySingleTon(String slogan) {
this.slogan = slogan;
}
private final static HungrySingleTon HUNGRY_SINGLE_TON = new HungrySingleTon("饿汉一号准备就绪");
public static HungrySingleTon getInstance() {
return HUNGRY_SINGLE_TON;
}
public static void main(String[] args) throws Exception {
HungrySingleTon hungrySingleTon1 = HungrySingleTon.getInstance();
HungrySingleTon hungrySingleTon2 = HungrySingleTon.getInstance();
System.out.println(hungrySingleTon1 == hungrySingleTon2);//true
// 通过反射使用HungrySingleTon类的构造器创建实例
Constructor<HungrySingleTon> constructor = HungrySingleTon.class.getDeclaredConstructor(String.class);
constructor.setAccessible(true);
HungrySingleTon reflectSingleTon = constructor.newInstance("反射饿汉一号准备就绪");
System.out.println(hungrySingleTon1 == reflectSingleTon);//false
}
}
饿汉式的缺点是浪费内存,如果定义了1000个饿汉式的单例模式,则 JVM启动时会立即为1000个饿汉式创建1000个实例对象。
上述饿汉式虽然保证了线程安全,但是也不是安全的单例模式的实现,可以通过反射破坏单例。
懒汉式
只有使用该对象时,才创建之。
package singleTon;
public class LazySingleTon {
private String slogan;
private LazySingleTon(String slogan) {
this.slogan = slogan;
}
private static LazySingleTon lazySingleTon;
public static LazySingleTon getInstance(){
if(lazySingleTon==null){
lazySingleTon=new LazySingleTon("懒汉一号准备就绪");
}
return lazySingleTon;
}
public static void main(String[] args) {
LazySingleTon lazySingleTon1 = LazySingleTon.getInstance();
LazySingleTon lazySingleTon2 = LazySingleTon.getInstance();
System.out.println(lazySingleTon1==lazySingleTon2);//true
}
}
如下图所示,懒汉式解决了JVM一启动就创建实例对象的问题,但存在线程安全问题。
package singleTon;
public class LazySingleTon {
private String slogan;
private LazySingleTon(String slogan) {
this.slogan = slogan;
System.out.println(Thread.currentThread().getName());
}
private static LazySingleTon lazySingleTon;
public static LazySingleTon getInstance() {
if (lazySingleTon == null) {
lazySingleTon = new LazySingleTon("懒汉一号准备就绪");
}
return lazySingleTon;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> LazySingleTon.getInstance(), "thread" + i).start();
}
/**
* 输出结果:
* thread0
* thread3
* thread2
* thread1
*/
}
}
DCL(Double Check Lock)
通过加锁解决懒汉式线程安全问题。
package singleTon;
public class DCLSingleTon {
private String slogan;
private DCLSingleTon(String slogan) {
this.slogan = slogan;
System.out.println(Thread.currentThread().getName());
}
//添加 volatile关键字 防止指令重排(dclSingleTon=new DCLSingleTon("DCL懒汉一号准备就绪");)
private static volatile DCLSingleTon dclSingTon;
public static DCLSingleTon getInstance(){
/**
* 这里有两次判断,逻辑上外部判断可以去除。
* 外部的判断是为了增加效率,如果instance不为空,就可以直接返回值,没有必要进入同步代码块。
*/
if(dclSingleTon==null){
synchronized (DCLSingleTon.class){
if(dclSingleTon==null){
dclSingleTon=new DCLSingleTon("DCL懒汉一号准备就绪");
}
}
}
return dclSingleTon;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->DCLSingleTon.getInstance(),"thread"+i).start();
}
/**
* 输出结果:
* thread0
*/
}
}
上述DCL虽然保证了线程安全,但是也不是安全的单例模式的实现,可以通过反射破坏单例。
public static void main(String[] args) throws Exception {
//获取DCL的实例
DCLSingleTon instance = DCLSingleTon.getInstance();
//通过反射使用DCLSingTon类的构造器创建实例
Constructor<DCLSingleTon> dclSingleTonConstructor = DCLSingleTon.class.getDeclaredConstructor(String.class);
//关闭安全检查
dclSingleTonConstructor.setAccessible(true);
// 获取实例
DCLSingleTon reflectInstance = dclSingleTonConstructor.newInstance("反射DCL懒汉一号准备就绪");
System.out.println(instance==reflectInstance);//false
}
静态内部类
当静态内部类被使用时,才会被加载,且只会加载一次,在加载时线程是安全的。
package singleTon;
import java.lang.reflect.Constructor;
public class staticInternalSingleTon {
private staticInternalSingleTon(String slogan) {
System.out.println(Thread.currentThread().getName());
}
// 当静态内部类被使用时,才会被加载,且只会加载一次,在加载时线程是安全的
public static class Inner {
private static final staticInternalSingleTon INSTANCE =
new staticInternalSingleTon("静态内部类一号准备就绪");
}
public static staticInternalSingleTon getInstance() {
return Inner.INSTANCE;
}
public static void main(String[] args) throws Exception {
// 通过反射使用staticInternalSingleTon类的构造器创建实例
Constructor<staticInternalSingleTon> declaredConstructor = staticInternalSingleTon.class.getDeclaredConstructor(String.class);
declaredConstructor.setAccessible(true);
staticInternalSingleTon staticInternalSingleTon = declaredConstructor.newInstance("反射静态内部类一号准备就绪");
System.out.println(getInstance() == staticInternalSingleTon);//false
}
}
上述静态内部类虽然保证了线程安全,但是也不是安全的单例模式的实现,可以通过反射破坏单例。
枚举
枚举类在类加载时创建,保证线程安全且不可以通过 (new,clone,反射,序列化)创建枚举实例。
package singleTon;
public enum EnumSingleTon {
INSTANCE;
public static EnumSingleTon getInstance() {
return INSTANCE;
}
public static void main(String[] args) throws Exception {
EnumSingleTon instance1 = EnumSingleTon.getInstance();
EnumSingleTon instance2 = EnumSingleTon.getInstance();
System.out.println(instance1.equals(instance2));//true
/*
通过反射获取实例
java.lang.IllegalArgumentException: Cannot reflectively create enum objects
不允许通过反射创建枚举对象
Constructor<EnumSingleTon> enumSingTonConstructor = EnumSingleTon.class.getDeclaredConstructor(String.class,int.class);
enumSingTonConstructor.setAccessible(true);
EnumSingleTon enumSingTon = enumSingTonConstructor.newInstance();
System.out.println(instance1.equals(enumSingTon));
*/
}
}
三、总结
- 单例模式是23个模式中比较简单的模式,应用也非常广泛,如在Spring中,每个Bean默认就是单例的,这样做的优点是Spring容器可以管理这些Bean的生命期。
- 单例模式的核心特点便是一个类仅有一个实例且自行实例化。
- 单例模式的实现方式应根据具体业务进行设计。