单例模式的概念
单例模式的意图是保证单例类在系统中有且仅有一个实例存在。
单例模式会自行实例化单例类,提供给整个系统使用。
单例模式的特点
有且仅有一个单例类实例(无法通过反序列化重新构建对象)构造函数私有化通过静态方法或枚举获得单例类实例
单例模式优点
严格控制对唯一的实例的访问方式(可以允许有限数量的实例)仅有一个实例,可以节约系统资源
单例模式缺点
单例模式没有抽象层,扩展比较困难职责过重,即充当工厂角色,有充当产品角色。如果长期不使用,Java会自动回收,导致下次使用又重新实例化。
单例模式的分类——根据实例的初始化时机
懒汉式:第一次获取单例类的实例时创建。饿汉式:在类被加载的时候创建单例类的对象,类加载器负责加载类,并会保证只有一个线程在实例化单例类,也可以保证另一个类在加载的时候如果需要使用到单例类的实例时,单例类已经被初始化了。
单例模式——懒汉方式——线程不安全版本
这种方式是线程不安全的,首先_instance没有被volatile关键字修饰,如果多个线程缓存了_instance(null)的值,则会创建多个单例类的对象;其次,多个线程在if (_instance == null) {之间切换的时候,也会创建多个单例类的对象。
单例模式——懒汉方式——线程安全版本(synchronized)
当多个线程同时调用getInstance()方法时,每次只有一个线程可以进入,其他的线程需要排队等待,对程序的执行效率有一定的影响。
单例模式——懒汉方式——线程安全(synchronized)双重检查(Double-Check)版本
线程安全版本还有一个问题,那就是单例类只实例化一次,必须要注意_instance变量已经被赋值是常态,在一个系统中,单例类的实例99%以上的时间都是处于已经实例化的状态,那么如何让获取单例类实例的操作尽可能少地上锁呢?
该版本中有两次if判断,这两次判断就是所谓的双重检查(Double-Check)。
第一次判断就是为了在单例类的实例被初始化之后不再进入同步块,这样可以避免在99%的时间内的同步需要。第二次判断看起来可能有点奇怪,但是想一下,第一次判断是没有加锁的,也就是说任何线程随时都可以进入,而创建单例类实例是需要一点时间的,在这个区间,可能有很多的线程已经执行完了第一个判断,排着队等待进入同步块。
单例模式——懒汉方式——线程安全(synchronized)双重检查(Double-Check)加volatile关键字版本
双重检查版本还有一点不足:
_instance = new Singleton()不是一个原子操作,JVM会首先给_instance分配内存,接下来可以把_instance指向已经分配的内存空间,也可以调用Singleton的构造函数进行实例化。
如果先把_instance指向已经分配的内存空间,那么_instance就不再是null,但是这个时候_instance只是指向已经分配的内存空间而已,如果其他线程执行完第一个判断就直接返回_instance,并调用_instance中的方法,则系统就会报错。
相反如果先进行初始化操作,则_instance还是null,就不会出现这种问题。
为了解决这个问题,可以对_instance使用volatile关键字修饰。
volatile关键字可以禁止指令重排序,即当Singleton没有初始化完成之前不会赋值给_instance。
单例模式——懒汉方式——静态内部类
当类Singleton被加载的时候,并不会实例化Singleton,因为初始化语句是写在内部类SingletonHolder中,如果没有用户主动调用getInstance()方法,Singleton是不会被实例化的,这样,我们可以控制单例类实例化的时间。
单例模式——饿汉方式
该模式在类被加载的时候实例化单例类,具体单例类什么时候被加载这个很难说,所以称这个模式为饿汉模式,它没有懒加载的效果。
单例模式——饿汉方式——枚举
之前的实现方式的缺点:
需要一定的额外工作来实现不能通过反序列化创建对象需要一定的额外工作来实现不能通过反射调用私有构造函数来创建对象
而枚举方式则没有这些缺点
最大的优点就是防止通过反射调用构造函数,和提供了自动序列化机制防止了通过反序列化重新创建新的对象。缺点是需要的内存两倍于静态常量。