整体介绍
介绍
- 单例模式(Singleton Pattern) 是 Java 中最简单的设计模式之一。
- 这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
- 意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
- 主要解决:一个全局使用的类频繁地创建与销毁。
- 何时使用:当您想控制实例数目,节省系统资源的时候。
- 如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
- 关键代码:构造函数是私有的。
优缺点
优点:
- 减少内存开销;尤其是频繁的创建和销毁实例,在内存里只有一个实例,可以减少开销。
- 避免资源占用;比如写文件操作,避免对资源的多重占用。
缺点:
- 没有接口,不能继承,与单一职责原则冲突。
- 一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
使用场景
- 取号器;要求生产唯一序列号。
- 计数器;WEB中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
- 连接池;创建的一个对象需要消耗的资源过多,比如 I/O 和数据库的连接等。
注意事项:
getInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。
写法简述
7种实现方式
- 懒汉式
- 懒汉式,线程安全
- 懒汉式,双重检验锁
- 饿汉式
- 饿汉式,变种
- 静态内部类
- 枚举
- 不过一般来说,第一种不算单例
- 第四种和第五种就是一种,如果算的话,第五种也可以分开写了。
所以说,一般单例都是五种写法。懒汉、饿汉、双重校验锁、静态内部类和枚举。
经验之谈
- 一般情况下,不建议使用"懒汉式"和"懒汉式,线程安全"这两种方式;建议使用"饿汉式"的方式。
- 只有在要明确实现懒加载效果时,才会使用"静态内部类"的方式。
- 如果涉及到反序列化创建对象时,可以尝试使用"枚举"的方式。
- 如果有其他特殊的需求,可以考虑使用"懒汉式,双重检验锁"的方式。
实现方式
懒汉式
优缺点
优点:
- 懒加载;延迟加载,即用时才加载
- 容易实现
缺点:
- 线程不安全;没有加锁,严格意义上它并不算单例模式
代码实现
1 | public class Singleton { |
懒汉式,线程安全
优缺点
优点:
- 懒加载;延迟加载,即用时才加载
- 容易实现
- 线程安全;使用synchronized加锁
缺点:
- 使用synchronized加锁;影响效率
代码实现
1 | public class Singleton { |
懒汉式,双重检验锁
优缺点
优点:
- 懒加载;延迟加载,即用时才加载
- 线程安全;效率高
缺点:
- 实现困难,潜在问题很多,后面分析
代码实现
1 | public class Singleton { |
潜在问题分析
这种方法貌似很完美的解决了上述效率的问题,它或许在并发量不多,安全性不太高的情况能完美运行,但是,这种方法也有不幸的地方。问题就是出现在这句 instance = new Singleton();
在JVM编译的过程中会出现指令重排的优化过程,这就会导致当instance实际上还没初始化,就可能被分配了内存空间,也就是说会出现 instance !=null 但是又没初始化的情况,这样就会导致返回的 instance 不完整。
我们来看看这个场景:假设线程一执行到instance = new SingletonKerrigan()这句,这里看起来是一句话,但实际上它并不是一个原子操作,这句话被编译成8条汇编指令,大致做了3件事情:
- 给实例分配内存。
- 初始化构造器
- 将instance对象指向分配的内存空间(注意到这步instance就非null了)。
但是,由于Java编译器允许处理器乱序执行(out-of-order),以及JDK1.5之前JMM(Java Memory Medel)中Cache、寄存器到主内存回写顺序的规定,上面的第二点和第三点的顺序是无法保证的,也就是说,执行顺序可能是1-2-3也可能是 1-3-2,如果是后者,并且在3执行完毕、2未执行之前,被切换到线程二上,这时候instance因为已经在线程一内执行过了第三 点,instance已经是非空了,所以线程二直接拿走instance,然后使用,然后顺理成章地报错,而且这种难以跟踪难以重现的错误很难找得出来。
在JDK1.5之后,官方已经注意到这种问题,因此调整了JMM、具体化了volatile关键字, 因此如果JDK是1.5或之后的版本,只需要将instance的定义改成“private volatile static SingletonKerriganD instance = null;”就可以保证每次都去instance都从主内存读取。当然volatile或多或少也会影响到性能。
饿汉式
优缺点
优点:
- 容易实现
- 线程安全
缺点:
- 实时加载,第一时间被创建;性能有缺陷
- 某些场景无法使用;譬如实例的创建是依赖参数或者配置文件的,在getInstance()之前必须调用某个方法设置参数给它。
代码实现
1 | public class Singleton { |
饿汉式,变种
优缺点
优点:
- 容易实现
- 线程安全
缺点:
- 非懒加载,类创建就被创建
- 某些场景无法使用;同第三种一样
代码实现
1 | public class Singleton { |
静态内部类
优缺点
优点:
- 懒加载;内部私有类,调用getInstance才会访问,才会加载创建。
- 线程安全;能达到双检锁方式一样的功效。
- 实现起来难度不大,中等难度
代码实现
1 | public class Singleton { |
枚举
优缺点
优点:
- 懒加载;性能高
- 容易实现
- 线程安全
- 单例实现的最佳方式,代码简洁
这种方式是Effective Java作者Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。
代码实现
1 | public enum SingletonEnum { |
创建单例实例的方式
直接new单例对象
一般我们会加入一个private或者protected的构造函数,这样系统就不会自动添加那个public的构造函数了,因此只能调用里面的static方法,无法通过new创建对象。
通过反射构造单例对象
如果单例由不同的类装载器装入,那便有可能存在多个单例类的实例。
假定不是远端存取,例如一些servlet容器对每个servlet使用完全不同的类装载器,这样的话如果有两个servlet访问一个单例类,它们就都会有各自的实例。
修复的办法是:
1 | private static Class getClass(String classname) throws ClassNotFoundException { |
通过序列化构造单例对象
如果单例对象有必要实现Serializable接口(很少出现),则应当同时实现readResolve()方法来保证反序列化的时候得到原来的对象。
写法如下:
1 | public class Singleton implements Serializable { |