介绍
定义:保证一个类仅有一个实例,并提供一个全局访问点。
类型:创建型
适用场景:想确保任何一个场景下都绝对只有一个实例,只能通过getInstace()方法来获取对象的唯一实例。
优点:
- 在内存中只有一个实例,减少了内存开销
- 可以避免对资源的多重占用
- 设置全局访问点,严格控制访问
- 没有接口,扩展困难
缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外部怎么样来实例化。
单例模式(Singleton Pattern)是Java中最简单的设计模式之一,提供了一种创建对象的最佳方式,但同时也是最复杂的设计模式之一。
这种设计模式设计到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
应用实例
- Windows是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。
- 一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。
单例模式的几种实现方式
1、懒汉式
是否Lazy初始化:是
是否多线程安全:否
实现难度:易
描述:
- 属于延迟初始化,这种方式只有在真正适用对象的时候才会初始化对象。
- 这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁synchronized,所以严格意义上它并不算单例模式。这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。
1
2
3
4
5
6
7
8
9
10
11public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
1.1、懒汉式,线程安全
是否Lazy初始化:是
是否多线程安全:是
实现难度:易
描述:
- 这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。因为不管对象实例是否存在,都会进入synchronized代码块
- 优点:第一次调用才初始化,避免内存浪费。
- 缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。 加锁本身不会带来多少性能消耗,性能消耗主要是在获取锁的过程,多个线程会因为锁的存在而市场出现阻塞的情况。
1
2
3
4
5
6
7
8
9
10
11
12public class Singleton {
private static Singleton instance;
private Singleton (){}
//静态方法加锁等于给类加锁,限制线程同时访问jvm中该类的所有实例同时访问对应的代码块。且一个类的所有静态方法公用一把锁。
//一般的synchronized(A a) ,是对A类的当前实例a进行加锁,防止其他线程同时访问该类的该实例a的所有synchronized块。
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
2、饿汉式
是否 Lazy 初始化:否
是否多线程安全:是
实现难度:易
优点:没有加锁,执行效率会提高。
缺点:类加载时就初始化,浪费内存。(操作系统启动后,不会启动所有的软件,而是当用户需要使用的时候才会去启动对应的软件)
描述:
- 这种方式比较常用,但容易产生垃圾对象。
- 基于 classloader 机制避免了多线程的同步问题
- 对于静态变量,只会在类加载的时候初始化一次,后面不会再变化。它不是一种懒加载模式(lazy initialization),单例会在加载类后一开始就被初始化,即使客户端没有调用getInstance()方法。饿汉式的创建方式在一些场景中将无法使用:譬如 Singleton 实例的创建是依赖参数或者配置文件的,在 getInstance()之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。
1
2
3
4
5
6
7public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
3、双检锁/双重校验锁(DCL,即 double-checked locking)
是否 Lazy 初始化:是
是否多线程安全:是
实现难度:较复杂
描述:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。高性能表现在,当对象实例不为null时,不会进入synchronized代码块。
1 | public class Singleton { |
这段代码看起来很完美,很可惜,它是有问题。主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。
- 给 instance 分配内存
- 调用 Singleton 的构造函数来初始化成员变量
- 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,锁被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回instance,然后使用,然后顺理成章地报错。
解决办法:
将 instance 变量声明成 volatile 就可以了。
4、登记式/静态内部类
是否 Lazy 初始化:是
是否多线程安全:是
实现难度:一般
推荐使用
描述:
- 这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟饿汉式不同的是:
- 饿汉式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果)。
- 而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。
- 想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比饿汉式就显得很合理。
1
2
3
4
5
6
7
8
9
10
11public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){
}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
5、枚举
是否 Lazy 初始化:否
是否多线程安全:是
实现难度:易
描述:
- 枚举类的构造方法只能在编译器调用,我们无法手动调用
- 类不能被继承,
- 构造器私有,
- 类变量是静态的,
- 通过静态块在类加载时就把对象初始化完成了,所以是线程安全的,
- 还有IO、反射相关的类对枚举类特殊对待,适当抛出异常。
1
2
3public enum EasySingleton{
INSTANCE;
}
6、容器单例模式
- 容器单例模式管理多个单例对象,统一管理,节省资源
- 通过map实现单例对象的存放,适合在容器初始化时,就把各个单例对象存放到容器中,之后就通过容器取出对象使用。
- 使用HashMap实现,会有并发问题的存在
- 使用HashTable实现,是线程安全的,但是影响性能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class SingletonManager {
private static Map<String,Object> map=new HashMap<String, Object>();
private SingletonManager(){}
public static void registerService(String key,Object instance){
if (!map.containsKey(key)){
map.put(key,instance);
}
}
public static Object getService(String key){
return map.get(key);
}
}
7、线程单例
通过隔离线程,不同的线程使用不同的对象,对于每个线程,自己的那个对象是单例的
参考:
- 本文作者: xczll
- 本文链接: https://xczllgit.github.io/2020/03/07/designPattern/2020-03-07-sigletonPattern/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!