单例设计模式
写在前面的话
- 本次笔记是在看了《毕向东Java基础》视频之后写的,对于毕老师讲课的优点,我想听过他的课的同学都应该知道的,所以我也不想多说了,但我还是忍不住再说一下,就是幽默风趣,讲课条理清晰,了解学生学习的难点,并能引领大家的思路。
- 另外,由于这是基础视频,所以讲的内容还是比较简单的。因此,我在课外进行了补充学习,希望能把这个单例模式问题学得更深更透!
什么是单例设计模式
什么是设计模式?
- 首先第一个问题就是:什么是单例设计模式?要弄懂这个问题,我们首先要理解什么是设计模式。设计模式原本并不是软件开发领域首创的,它是建筑领域的一套理论知识。所谓设计模式,就是解决同一类问题的一种方法,它面向的是高层的抽象理念,和Java中的类和实例之间的关系有相似之处。
单例设计模式解决的问题
- 单例设计模式面向的问题是什么呢?保证内存中某个类的实例的唯一性。即是说,无论什么时候,在内存中只能存在一个实例。
哪里应用了单例设计模式
- 这样的设计需要有哪些呢?举些例子来说吧,操作系统当中的垃圾桶,它在系统运行过程中都只有一个实例存在,无论你在C盘删除文件,还是在D盘删除文件,文件垃圾都是保存到同一个垃圾桶实例当中。这个最简单的验证就是:若果你把文件属性中的隐藏文件选择显示,那么你会在系统的任何地方都有一个垃圾桶文件夹,而这并不是说有多个垃圾桶实例,而是一个垃圾桶实例的多个引用。
- 另外的例子还包括软件的配置文件,其实也是应用了单例设计模式。试想下,若果软件的配置的配置有多个,那么软件在下次运行时应该选择哪么配置文件呢?比如eclipse,你在使用时发现没有显示行数,那么狠明显你是修改配置,行号就可以显示出来,而当你下次启动eclipse时,行号还是显示的。所有配置文件只有一个,你任何的修改都是对一个配置文件进行,这样才能保证下次启动软件时持续生效。
单例设计模式的实现(Java)
单例设计模式的思路
- 为了避免其他程序过多的建立该类对象。先禁止其他程序建立该类对象
- 为了让其他程序可以访问到该类对象,只好在本类中,自定义一个对象。
- 为了方便其他程序对自定义对象的访问,可以对外提供一些访问方式。
单例设计模式的代码实现步骤
- 将构造函数私有化
- 在类中创造一个本类对象
- 提供一个方法可以获取到该对象
实现代码
1 | public class SingletonDemo { |
单例设计模式就那么简单吗?
- 咋看,觉得代码那么简单,其实并不是那么简单,其中的坑还是不少的。让我们逐层逐层揭开它的神秘面纱吧。
- 单例设计模式一般有2种形式:懒汉式和饿汉式
懒汉式单例设计模式
1 | // 懒汉式 |
- 上面的代码看上去似乎没什么问题?但其实暗藏了一个安全问题,因为在getInstance()的方法中包含2个语句,并不是原子操作,存在线程安全的问题,导致可能产生多个实例对象。
- 线程安全的懒汉式单例设计模式(只改getInstance方法)
1
2
3
4
5
6public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
} - 以上的代码,在方法前面加了synchronized关键字,表示了线程安全。但是把整个方法都同步了之后,程序的运行速度变得更慢了。为了解决慢的问题,又引出了双重检验锁。
- 双重检验锁(double checked locking pattern)的懒汉式单例设计模式(只改getInstance方法)
1
2
3
4
5
6
7
8
9
10
11public static Singleton getSingleton() {
if (instance == null) { // Single checked
synchronized (Singleton.class) {
if (instance == null) { // Double checked
instance = new Singleton();
}
}
}
return instance;
} - 这段代码貌似很完美了,但很遗憾地,它还是有问题的。主要是instance = new Singleton()这句,这并非是一个原子操作,事实上在JVM中执行这个语句时做了以下3个事情。
- 给instance分配内存
- 调用Singleton的构造函数来初始化成员变量
- 将instance对象指向分配的内存空间(执行完这步instance才是非空)
- 但在JVM的即时编译器中存在指令重排序的优化。也就是上面的第2步和第3步的顺序是不能保证的,最终的执行顺序可能是1-2-3,也可能是1-3-2.如果是后者,则在3执行完毕、2未执行前,被线程二抢占了,这时的instance已经是非null(但却没有初始化),所以线程二会直接返回instance,然后使用,然后顺理成章地报错了。(摘抄自大牛原文)
- 那我们应该怎么办呢?我们把instance变量声明为volatile就可以了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class Singleton {
private volatile static Singleton instance;
private Singleton() {}
private static Singleton getSingleton() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
} - (大牛解释)
- 有些人认为使用 volatile 的原因是可见性,也就是可以保证线程在本地不会存有 instance 的副本,每次都是去主内存中读取。但其实是不对的。使用 volatile 的主要原因是其另一个特性:禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完 1-2-3 之后或者 1-3-2 之后,不存在执行到 1-3 然后取到值的情况。从「先行发生原则」的角度理解的话,就是对于一个 volatile 变量的写操作都先行发生于后面对这个变量的读操作(这里的“后面”是时间上的先后顺序)。
- 但是特别注意在 Java 5 以前的版本使用了 volatile 的双检锁还是有问题的。其原因是 Java 5 以前的 JMM (Java 内存模型)是存在缺陷的,即时将变量声明成 volatile 也不能完全避免重排序,主要是 volatile 变量前后的代码仍然存在重排序问题。这个 volatile 屏蔽重排序的问题在 Java 5 中才得以修复,所以在这之后才可以放心使用 volatile。
- 相信你不会喜欢这种复杂又隐含问题的方式,当然我们有更好的实现线程安全的单例模式的办法。
饿汉式单例设计模式
1 | // 饿汉式 |
- 这种方法非常简单,因为单例的实例被声明成static和final变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。
- 但是这种方法还是有问题的,它的缺点是它不是一种懒加载模式(lazy initialization),单例会在加载类后一开始就被初始化,即使客户端没有调用getInstance()方法。饿汉式的创建方式在一些场景中将无法使用:譬如Singleton实例的创建时依赖参数或者配置文件的,在getInstance()之前必须调用某个方法设置参数给它,那么这种单例写法就无法使用了。
静态内部类的单例设计模式
1 | public class Singleton { |
- 这种写法仍然使用JVM本身机制保证了线程安全问题;由于SingletonHolder 是私有的,除了getInstance()之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖JDK版本。
枚举Enum
- 用枚举来实现单例真的太简单了!
1
2
3public enum EasySingleton {
INSTANCE;
} - 我们可以通过EasySingleton.INSTANCE来访问实例,这比调用getInstance()方法简单多了。创建枚举默认就是线程安全的,所以不需要担心double checked locking,而且还能防止反序列化导致重新创建新的对象。但是还是很少看到有人这样写,可能是因为不太熟悉吧。
后记
- 一般来说,在开发中常用的是饿汉式的单例模式。
- 参考:如何正确地写出单例模式
- 本文标题:单例设计模式
- 创建时间:2014-12-25 16:30:38
- 本文链接:2014/12/25/alogrithms/Singleton-note/
- 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
评论