单例模式(Singleton)是属于设计模式三大分类中的创建型模式,顾名思义,就是对一个类如何创建一个对象到系统中的一种设计思想。所谓单例,即单个实例,就是指在整个我们软件系统中此类有且最多只能有一个该类型的实例。
单例模式一般作为我们在学习面向对象设计模式中第一个设计,在于此模式相对于易理解,但是并不意味着这是最简单的设计模式,在我眼里单例模式相对于难度是适中的,而且如何能够应用好、应用巧更是考验一个工程师的编码功底,接下来就开始讲解一下单例模式。
1. 概念
一个单一的类,负责创建自己的对象,同时确保系统中只有单个对象被创建。
单例特点:
- 某个类只能有一个实例;(构造器私有)
- 它必须自行创建这个实例;(自己编写实例化逻辑)
- 它必须自行向整个系统提供这个实例;(对外提供实例化方法)
2. 代码实现
那么根据我们上方所介绍的单例模式的概念以及其特点,我们可以进行编码实现:
/**
* 单例设计模式:Version-1
*/
public class Singleton {
// 模拟该类的属性
private Object attribute;
private static Singleton instance;
// 满足构造器私有
private Singleton() {
System.out.println("Create the instance...");
}
// 自行编写创建这个实例并对系统进行提供
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
以上是我们按照定义以及特点实现的一个简单的单例类,但是经验丰富的开发者会发现这个类还存在着一些致命的问题。
3. 并发问题
在上述的代码实现中,这段代码在系统中真的是单例的吗?其实这段代码表面看是符合了单例模式设计的定义以及其特点,但是如果在多线程的场景下,有两个线程在同一时间共同调用了getInstance(),那么就会出现并发问题,导致两个线程在判断instance都为null,则每个线程都创建了一个新的instance回来,这样导致其实在JVM虚拟机中的堆空间中会存在两个instance的实例(其实后续JVM会回收一个实例)。一提到多线程问题其实解决方法也就明确了,就是加锁来进行,接下来是解决后的代码:
/**
* 单例设计模式:Version-2 (双重检查锁保证多线程环境下无问题)
*/
public class Singleton {
// 模拟该类的属性
private Object attribute;
private static Singleton instance;
// 满足构造器私有
private Singleton() {
System.out.println("Create the instance...");
}
// 自行编写创建这个实例并对系统进行提供
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
// 双重检查锁
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
我们通过双重检查锁来完善以上代码即可解决多线程并发出现的问题。
4. 懒汉式与饿汉式
所谓懒汉式与饿汉式是从单例模式在何时创建对象的角度上进行分类的,懒汉式突出在“懒”,也就是别人不需要我这个对象的时候我就懒得为系统创建出来,等你什么时候需要,我在什么时候创建,上述我实现的代码就是懒汉式,只有系统中其他人需要我这个Singleton的时候,通过getInstance方法获取的时候我才进行创建。
饿汉式突出点在于“饿”,即我不论如何先让我自己饿到,也就是我不管别人如何我先创建好我这个实例,随时准备着为系统其他人服务,饿汉式相对来说我是不会使用的,因为上来就创建对象虽然实现过程简单,通常也不会存在并发问题,但是如果此实例从未被使用会造成资源浪费(虽然有些人认为这点浪费是虚无缥缈的,但是作为开发者而言在成长的路上也要去认真的对待)。那么接下来的代码就是改造成饿汉式:
public class Singleton {
// 模拟该类的属性
private Object attribute;
// 在类加载时就创建实例
private static final Singleton instance = new Singleton();
// 满足构造器私有
private Singleton() {
System.out.println("Create the instance...");
}
// 提供全局访问点
public static Singleton getInstance() {
return instance;
}
}
5. 拓展:枚举实现单例模式
单例模式可能对于初级开发人员讲到上述就结束了,但是如果想进一步拓展的话就不得不讲讲通过枚举类来实现的单例模式了。相对于上述我们通过加锁来保证并发安全问题,枚举类由于其在类加载时由JVM自动创建的,直接在JVM级别上就保证了线程安全;同时枚举类实现的方式也防止反射攻击(枚举类型不允许通过反射机制创建新的实例),这样可以防止反射破坏了单例模式;最后枚举类型在序列化和反序列化中自动处理,确保反序列化后的对象仍然是同一个实例,这样可以防止序列化攻击。最后是代码相关实现:
public class SingletonExample {
public static void main(String[] args) {
// 获取单例实例
Singleton singleton1 = Singleton.INSTANCE;
Singleton singleton2 = Singleton.INSTANCE;
// 验证是否是同一个实例
System.out.println(singleton1 == singleton2); // 输出: true
// 访问和修改属性
System.out.println(singleton1.getAttribute());
singleton1.setAttribute(new Object());
System.out.println(singleton2.getAttribute());
}
}
6. 应用场景
如果想用好单例模式,我们必须清除了解到什么场景下会用到单例设计模式:
- 多线程中的线程池:线程池在整个系统中一定是唯一的。
- 数据库的连接池
- 系统环境信息
- 上下文信息:如ServletContext等