单例模式的几种实现方式总结
2016-12-13 23:24
519 查看
单例模式可能是设计模式中最容易理解,应用最广泛的模式。单例模式虽然简单,但其中的坑却不少,尤其是线程安全问题。本文主要对单例的各种写法做个总结,并分析其优缺点。
在Java中,单例模式的实现分两种,一种称为懒汉式,一种称为饿汉式,其实就是在具体创建对象实例的处理上,有不同的实现方式。
这段代码简单明了,但是却存在致命的问题。当有多个线程并行调用 getInstance() 的时候,就会创建多个实例。也就是说在多线程下不能正常工作。
为了解决线程安全问题,最简单的方法是将整个 getInstance() 方法设为同步(synchronized),代码如下:
上述做法虽然解决了线程安全问题,但是它并不高效,因为在任何时候只能有一个线程调用 getInstance() 方法。
改进的办法就是:在真正需要同步的地方才加锁,即第一次创建单例实例对象时。使用 双重检查加锁(double-check)。
双重检查加锁(double checked locking pattern)是一种使用同步块加锁的方法。程序员称其为双重检查锁,因为会有两次检查 instance == null,一次是在同步块外,一次是在同步块内。代码如下:
如上面代码所示,如果第一次检查instance不为null,那么就不需要执行下面的加锁和初始化操作。因此可以大幅降低synchronized带来的性能开销。上面代码表面上看起来,似乎两全其美:
双重检查锁定看起来似乎很完美,但这是一个错误的优化!在线程执行到第2行代码读取到instance不为null时,instance引用的对象有可能还没有完成初始化。
问题的根源
前面的双重检查锁定示例代码的第7行(instance = new Singleton();)创建一个对象。这一行代码可以分解为如下的三行伪代码:
但是在 JVM 的即时编译器(JIT)中存在指令重排序的优化。上面三行伪代码中的2和3之间,可能会被重排序,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化)导致最终出错。
解决办法:只需要将 instance 变量声明成 volatile 就可以了,禁止指令重排序优化。
注意:
在Java 1.5之前版本中,很多JVM对于volatile
关键字的实现有问题,会导致双重检查加锁的失败,因此双重检查加锁的机制只能用在Java 5及以上的版本。
代码如下:
这种实现方式由JVM来保证线程的安全性,另外,由于是在静态内部类SingletonHolder里面去创建对象,只要不使用这个静态内部类,就不会创建对象实例 ,因此它属于懒汉式;
使用枚举来实现单例会更加简洁,而且无偿提供了序列化的机制,并由JVM从根本上提供保障,绝对防止多次实例化,是更简洁、高效、安全的实现单例的方式。
模式定义
保证一个类仅有一个实例,并提供一个访问它的全局访问点。在Java中,单例模式的实现分两种,一种称为懒汉式,一种称为饿汉式,其实就是在具体创建对象实例的处理上,有不同的实现方式。
代码实现
1、懒汉式实现
当被问到要实现一个单例模式时,大多数人的第一反应是写出如下的代码:/** * 懒汉式(非线程安全) * * @author Ricky Fung * @create 2016-12-13 22:37 */ public class Singleton { private static Singleton instance; private Singleton(){ } /**非线程安全*/ public static Singleton getInstance(){ if(instance==null){ instance = new Singleton(); } return instance; } }
这段代码简单明了,但是却存在致命的问题。当有多个线程并行调用 getInstance() 的时候,就会创建多个实例。也就是说在多线程下不能正常工作。
为了解决线程安全问题,最简单的方法是将整个 getInstance() 方法设为同步(synchronized),代码如下:
/** * 懒汉式(线程安全) * * @author Ricky Fung * @create 2016-12-13 22:37 */ public class Singleton { private static Singleton instance; private Singleton(){ } public static synchronized Singleton getInstance(){ if(instance==null){ instance = new Singleton(); } return instance; } }
上述做法虽然解决了线程安全问题,但是它并不高效,因为在任何时候只能有一个线程调用 getInstance() 方法。
改进的办法就是:在真正需要同步的地方才加锁,即第一次创建单例实例对象时。使用 双重检查加锁(double-check)。
双重检查加锁(double checked locking pattern)是一种使用同步块加锁的方法。程序员称其为双重检查锁,因为会有两次检查 instance == null,一次是在同步块外,一次是在同步块内。代码如下:
public class Singleton { private static Singleton instance; private Singleton(){ } /**双重检查(double-check)**/ public static Singleton getInstance(){ //1 if(instance==null){ //2 synchronized (Singleton.class){ //3 if(instance==null){ //4 instance = new Singleton(); //5 } } } return instance; } }
如上面代码所示,如果第一次检查instance不为null,那么就不需要执行下面的加锁和初始化操作。因此可以大幅降低synchronized带来的性能开销。上面代码表面上看起来,似乎两全其美:
双重检查锁定看起来似乎很完美,但这是一个错误的优化!在线程执行到第2行代码读取到instance不为null时,instance引用的对象有可能还没有完成初始化。
问题的根源
前面的双重检查锁定示例代码的第7行(instance = new Singleton();)创建一个对象。这一行代码可以分解为如下的三行伪代码:
memory = allocate(); //1:分配对象的内存空间 ctorInstance(memory); //2:初始化对象 instance = memory; //3:设置instance指向刚分配的内存地址
但是在 JVM 的即时编译器(JIT)中存在指令重排序的优化。上面三行伪代码中的2和3之间,可能会被重排序,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化)导致最终出错。
解决办法:只需要将 instance 变量声明成 volatile 就可以了,禁止指令重排序优化。
/** * 懒汉式(线程安全) * * @author Ricky Fung * @create 2016-12-13 22:37 */ public class Singleton { private volatile static Singleton instance; private Singleton(){ } /**双重检查(double-check)**/ public static Singleton getInstance(){ if(instance==null){ synchronized (Singleton.class){ if(instance==null){ instance = new Singleton(); } } } return instance; } }
注意:
在Java 1.5之前版本中,很多JVM对于volatile
关键字的实现有问题,会导致双重检查加锁的失败,因此双重检查加锁的机制只能用在Java 5及以上的版本。
2、饿汉式
这种方法非常简单,因为单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。代码如下:
/** * 饿汉式(线程安全) * * @author Ricky Fung * @create 2016-12-13 22:37 */ public class Singleton { private static final Singleton INSTANCE = new Singleton(); private Singleton(){ } public static Singleton getInstance(){ return INSTANCE; } }
更高效、安全的实现方式
1、静态内部类
这个方案被称为 Lazy initialization holder class模式,代码如下:/** * 懒汉式(线程安全) * * @author Ricky Fung * @create 2016-12-13 22:37 */ public class Singleton { private Singleton(){ } public static Singleton getInstance(){ return SingletonHolder.INSTANCE; } private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } }
这种实现方式由JVM来保证线程的安全性,另外,由于是在静态内部类SingletonHolder里面去创建对象,只要不使用这个静态内部类,就不会创建对象实例 ,因此它属于懒汉式;
2、枚举
按照《Effective Java 第二版》中的说法:单元素的枚举类型已经成为实现Singleton的最佳方法。/** * 枚举实现单例 * * @author Ricky Fung * @create 2016-12-13 22:39 */ public enum Singleton { INSTANCE; public void doSth(){ //do stuff } }
使用枚举来实现单例会更加简洁,而且无偿提供了序列化的机制,并由JVM从根本上提供保障,绝对防止多次实例化,是更简洁、高效、安全的实现单例的方式。
参考
双重检查锁定与延迟初始化:http://www.infoq.com/cn/articles/double-checked-locking-with-delay-initialization相关文章推荐
- 结合Android源码分析总结单例模式的几种实现方式
- URL重写的几种实现方式的总结
- 单例模式singleton的几种实现方式
- PHP实现单件模式的几种方式
- 关于jsp和servlet中实现页面跳转的几种方式总结
- URL重写的几种实现方式的总结
- URL重写的几种实现方式的总结
- 【转】单例模式的几种实现方式及其特点
- 用java实现邮件服务的几种方式总结
- 订单的几种实现方式(用不同的模式实现:装饰器模式、代理模式、命令模式、状态模式、模版模式)
- Item 3 ------单例模式的几种实现方式,及优缺点
- singleton pattern(单件模式)的几种实现方式
- URL重写的几种实现方式的总结
- java设计模式学习笔记5 适配器模式几种实现方式
- URL重写的几种实现方式的总结
- 设计模式-PHP实现单件模式的几种方式
- 设计模式-PHP实现单件模式的几种方式
- 设计模式-PHP实现单件模式的几种方式
- 设计模式用过哪些,应用场景是什么;单例模式有几种实现方式,代码怎么写?
- JAVA中单例模式的几种实现方式