单例模式几种实现方式
1、饿汉式:静态常量
特点:单例的实例被声明成static和final变量了,在第一次加载类到内存中时就会初始化,所以会创建实例本身是线程安全的
public class Singleton { private final static Singleton instance = new Singleton(); private Singleton(){ } public static Singleton getInstance(){ return instance; } }
2、懒汉式:线程不安全
特点:使用了懒加载模式,但是却存在致命的问题。当多个线程并行调用getInstance()的时候,就会创建多个实例,即在多线程下不能正常工作
public class Singleton { private static Singleton instance = null; private Singleton(){ } public static Singleton getInstance(){ if(null == instance) { instance = new Singleton(); } return instance; } }
3、懒汉式:线程安全
特点:线程安全,并且解决了多实例的问题,但是它并不高效。因为在任何时候只能有一个线程调用getInstance()方法,但是同步操作只需要在第一次调用时才被需要,即第一次创建单例实例对象时
public class Singleton { private static Singleton instance = null; private Singleton(){ } public static synchronized Singleton getInstance(){ if(null == instance) { instance = new Singleton(); } return instance; } }
4、懒汉式:静态内部类
特点:使用JVM本身机制保证了线程安全问题;由于SingleHolder是私有的,除了getInstacne()之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷,也不依赖JDK版本
public class Singleton { private Singleton(){ } private static class SingleHolder{ private static final Singleton instance = new Singleton(); } public static Singleton getInstance(){ return SingleHolder.instance; } }
5、双重检查锁
特点:是一种使用同步块加锁的方法。又称其为双重检查锁,因为会有两次检查instance == null,一次是在同步块外,一次是在同步快内。为什么在同步块内还要检验一次,因为可能会有多个线程一起进入同步块外的if,如果在同步块内不进行二次检验 的话就会生成多个实例了
public class Singleton { //volatile 防止指令重排序 private static volatile Singleton instance; //双重锁 public static Singleton getInstance() { if(null == instance) { synchronized(Singleton.class){ if(null == instance) { instance = new Singleton(); } return instance; } } return instance; } }
问题:这样的写法在很多平台和优化编译器上是错误的。
原因在于:instance = new Singleton ()并非是原子操作,事实上在JVM中这句话做了三件事:
1.instance = 给新的实体分配内存
2.调用Singleton的构造函数来初始化instance的成员变量
3.将instance对象指向分配的空间(执行完这一步instance就为null)
现在想象一下有线程A和B在调用getInstance,线程A先进入,在执行到步骤1的时候被踢出了cpu。然后线程B进入,B看到的是instance 已经不是null了(内存已经分配),于是它开始放心地使用instance,但这个是错误的,因为在这一时刻,instance的成员变量还都是缺省值,A还没有来得及执行步骤2来完成instance的初始化。
当然编译器也可以这样实现:
1. temp = 分配内存
2. 调用temp的构造函数
3. instance = temp
如果编译器的行为是这样的话我们似乎就没有问题了,但事实却不是那么简单,因为我们无法知道某个编译器具体是怎么做的,因为在Java的memory model里对这个问题没有定义。
双检锁对于基础类型(比如int)适用。很显然吧,因为基础类型没有调用构造函数这一步。
6、枚举
特点:通过EasySingleton.INSTANCE来访问实例,这比调用getInstance()方法简单多了。创建枚举默认就是线程安全,而且还能防止反序列化导致重新创建新的对象
public class Singleton { public enum EasySingleton{ INSTANCE; } }