✏️ 一思尚存,此志不懈。——胡居仁
一个类只允许创建一个对象(或者实例),那么这个类就是一个单例类。这种模式也叫单例模式。
实现一个单例,需要关注以下几个点:
- 构造函数需要是
private
访问权限的,避免外部通过new
创建实例。 - 考虑对象创建的线程安全问题。
- 考虑是否支持延迟加载。
- 考虑
getInstance()
性能是否高(是否加锁)。
- 需要频繁创建的一些类,使用单例可以降低系统的内存压力,减少GC
- 某些类创建实例时占用资源较多,或实例化耗时较长,且经常使用
- 频繁访问数据库或文件的对象
饿汉式的单例,是指在类加载的时候,instance
静态实例就已经创建并初始化好了。所以instance
实例创建的过程是线程安全的。
不过这种方式不支持延迟加载。
饿汉单例具体实现如下:
public class HungrySingleton {
private static final HungrySingleton instance = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return instance;
}
}
因为饿汉单例是在类加载时创建,因此系统中存在大量的饿汉式单例的话,就会造成内存浪费的问题。因此,使用饿汉模式的话,需要根据实际情况来考虑。比如存在以下的情况,可以采用饿汉模式:
- 如果单例初始化耗时长,则可以将这部分耗时提前到类加载时消耗。
- 如果实例占用资源过多,根据fail-fast原则,这些实例也要使用饿汉模式。
!> 饿汉单例,是线程安全的。
懒汉式的单例是与饿汉式单例相对的,懒汉式单例支持延迟加载,具体实现如下:
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {
}
private LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
但是这种实现方式是线程不安全的,我们可以编写一个多线程的程序对单例进行测试,如下:
@Slf4j
class LazySingletonTest {
@Test
void createLazySingletonTest() {
Thread thread1 = new Thread(new CreateLazySingletonRunnable());
Thread thread2 = new Thread(new CreateLazySingletonRunnable());
thread1.start();
thread2.start();
}
static class CreateLazySingletonRunnable implements Runnable {
@Override
public void run() {
LazySingleton instance = LazySingleton.getInstance();
log.info("current Thread Name:{} current singleton:{}", Thread.currentThread().getName(), instance);
}
}
}
输出结果如下:
current Thread Name:Thread-2 current singleton:com.lvan.design_mode.singleton.LazySingleton@41ce6b13
current Thread Name:Thread-1 current singleton:com.lvan.design_mode.singleton.LazySingleton@50aa1827
通过以上结果,已经验证了多线程环境会对以上单例进行破坏。
为了解决线程不安全的问题,我们使用synchronized
来给getInstance
加锁,代码如下:
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {
}
public static synchronized LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
再运行以上多线程获取单例对象,通过结果就可以发现,此时懒汉式的单例已经是线程安全的了。
但是,使用synchronized
加锁,会导致使用者每次调用getInstance()
方法时,会导致频繁获取锁、释放锁等处理,在并发量高的情况下,会存在性能地瓶颈。实际上,多线程竞争的情况,只会在单例初次加载的时候,也就是说,单例加载完成后,再调用getInstance()
方法,就不需要synchronized
来加持了。
由于synchronized
目前是加在方法的级别上的,我们可以将其限制在单例初次加载时,代码设计如下:
public class LazySingleton {
private static volatile LazySingleton instance;
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (instance == null) {
synchronized (LazySingleton.class) {
if (instance == null) {
instance = new LazySingleton();
}
}
}
return instance;
}
}
这种实现单例模式的,也叫做Double-check。
但是这种方式,也是存在指令重排序的问题的,主要是因为new LazySingleton()
这句代码的执行,并非一个原子性操作,它其实包括了三个操作,分别是分配对象的内存空间、初始化对象、设置instance
指向刚分配的内存地址,也正是因为指令重排序的原因,初始化对象与设置instance
指向为对象分配的内存地址这两个操作并非具有顺序性的,也就是说instance
指向内存地址可以在初始化对象前执行,那么在多线程的情况下,可能会导致某些线程访问到未初始化的对象。
因此,为了解决指令重排序的问题,我在instance
变量的声明时,使用volatile
来解决了这个问题。
双重检查锁单例写法虽然解决了线程安全问题和性能问题,但是只要用到LazySingleton
关键字就是还是利用到锁,对性能还是存在一定影响的。
静态内部类实现单例是一种比较简单的实现方式,也能实现单例的懒加载,代码设计如下:
public class StaticInnerSingleton {
private StaticInnerSingleton() {
}
private static class SingletonHolder {
private static final StaticInnerSingleton INSTANCE = new StaticInnerSingleton();
}
public static StaticInnerSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
通过静态内部类实现单例,主要是利用Java语法的特性。当外部类StaticInnerSingleton
被加载的时候,并不会创建SingletonHolder
实例对象,只有调用getInstance()
方法时,SingletonHolder
才会被加载,这个时候才会创建instance
。instance
的唯一性、创建过程的线程安全性都有JVM来保证。
所以,这种方式既保证了线程安全,又能做到延迟加载。
不管是饿汉、懒汉、还是静态内部类实现的单例,我们都可以通过反射来修改构造函数的可见性,以此来破坏单例对象。
难道就没有比较安全的方法来实现单例吗?那就是通过枚举的特性,来实现单例。代码设计如下:
public enum SingletonEnum {
INSTANCE;
@Getter
private Object Object;
public static SingletonEnum getInstance() {
return INSTANCE;
}
}
通过枚举来是实现单例,保证了实例创建的线程安全性和实例的唯一性。
Spring框架中管理的对象基本都是单例对象,这些单例对象存储在Map数据结构中,因此这些单例也称之为容器式单例,比较适用于大量创建单例对象的场景。
通过反射,我们可以修改构造函数的访问控制,再通过构造函数来创建对象,这样就可以破坏单例了,作案代码如下:
@Test
void reflectDestroySingleton() throws Exception {
Class<LazySingleton> lazySingletonClass = LazySingleton.class;
Constructor<LazySingleton> declaredConstructor = lazySingletonClass.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
LazySingleton lazySingleton = declaredConstructor.newInstance();
LazySingleton lazySingleton1 = declaredConstructor.newInstance();
assertThat(lazySingleton).isEqualTo(lazySingleton1);
}
输出结果如下:
Expected :com.lvan.design_mode.singleton.LazySingleton@7c417213
Actual :com.lvan.design_mode.singleton.LazySingleton@6107227e
通过以上结果,可以看出我们创建了两个不同的实例,也就破坏了单例。
将单例进行序列化保存,再进行反序列化读取这种情况一般很少出现,除非这个单例在整个应用集群下是单例的(默认的单例是在Jvm进程下保持单例)。
但是,反序列化也是可以破坏单例的。作案代码如下:
!> LazySingleton需要实现Serializable接口
@Test
void serializableDestroySingleton() throws Exception {
LazySingleton origObj = LazySingleton.getInstance();
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("singleton.obj"));
outputStream.writeObject(origObj);
outputStream.flush();
outputStream.close();
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("singleton.obj"));
LazySingleton obj = (LazySingleton) inputStream.readObject();
assertThat(obj).isEqualTo(origObj);
}
输出结果如下:
Expected :com.lvan.design_mode.singleton.LazySingleton@5cdd8682
Actual :com.lvan.design_mode.singleton.LazySingleton@1a0dcaa
通过以上结果,可以反序列化得到的实例对象与原对象不相等。
但是,这种方式,我们是可以避免的,序列化提供了这样的一个切入点,可以帮助我们在反序列化时,也能够保护单例对象。代码如下:
private Object readResolve() {
return this.instance;
}
- 单例可以保证内存里只有一个实例,减少内存的开销
- 单例可以避免对资源的多重使用
- 单例模式可以设置全局访问点,可以优化和共享资源访问
- 单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背开闭原则
- 在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象
- 单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则