单例模式

1. 使用场景

当希望整个JVM中仅有此类的一个实例时。

2. 实现方式

饿汉式A

class Singleton { private static Singleton me = new Singleton(); private Singleton() {} public static Singleton getInstance() { return me; } }

要点

  • 构造私有,别的类无法创建实例
  • 在类加载时就创建本类唯一实例
  • 因为类加载只执行一次,没有多线程并发问题
  • 可以用反射破坏单例,此问题同样存在于下面几种懒汉式的实现
  • 可以用反序列化破坏单例的,此问题同样存在于下面几种懒汉式的实现 (可解决)

饿汉式B

enum Singleton { INSTANCE; }

要点

  • enum 本身特性保证了实例个数
  • 没有多线程并发问题,没有反射破坏单例问题,没有反序列化方法破坏单例问题

懒汉式A

class Singleton { private static Singleton me = null; private Singleton() { } public static synchronized Singleton getInstance() { if( me == null ){ me = new Singleton(); } return me; } }

要点

  • 构造私有,别的类无法创建实例
  • 在类加载时暂时不创建实例,而是在第一次用到时创建
  • 如果首次使用 getInstance() 方法是多线程并发,必须添加synchronized 保证线程安全
  • 此实现有缺陷:本来只是在首次使用 getInstance() 时才有并发问题,但这个实现是次次都需要synchronized 效率低下

懒汉式B

class Singleton { private static volatile Singleton me = null; private Singleton() { } public static Singleton getInstance() { if (me == null) { synchronized (Singleton.class) { if (me == null) { me = new Singleton(); } } } return me; } }

要点

  • 构造私有,别的类无法创建实例
  • 在类加载时暂时不创建实例,而是在第一次用到时创建
  • 首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁
  • volatile 关键字是为了防止指令重排,只有在JDK 5 以上的版本有效

指令重排问题分析

首先要明白jvm会对代码进行优化,进行指令重排,例如 me = new Singleton(); 实际可以分别三步:

1) 分配空间,生成了引用地址
2) 调用构造方法
3) 将引用地址赋值给me变量

其中 2) 3) 两步的顺序不是固定的,也许jvm会优化为:先将引用地址赋值给me变量后,再执行构造方法。

如果两个线程t1, t2按如下时间序列执行:

时间1  t1 线程执行到 me = new Singleton(); 
时间2  t1 线程分配空间,为Singleton对象生成了引用地址
时间3  t1 线程将引用地址赋值给me,这时me != null
时间4  t2 线程进入getInstance() 方法,发现 me != null,直接返回me
时间5  t1 线程执行Singleton的构造方法

这时t1 还未完全将Singleton的构造方法执行完毕,如果在Singleton的构造方法中要执行很多初始化操作,那么t2拿到的是很可能是一个不完整的单例。

问题解决

volatile 修饰的变量不会参与指令重排,因此上述问题得到解决,但要注意在JDK 5 以上的版本volatile才会真正发挥作用。

懒汉式C

class Singleton { private Singleton() { } private static class LazyHolder { private static Singleton me = new Singleton(); } public static Singleton getInstance() { return LazyHolder.me; } }

要点

  • 构造私有,别的类无法创建实例
  • 在类加载时暂时不创建实例,而是在第一次用到时创建
  • 使用了静态内部类是用到时才加载来实现懒汉实例化,并且因为内部类的加载只会执行一次,从而能保证线程安全

3. 不使用理由

单例模式由于其简单易实现,为初学者所广泛使用,最容易被误用和滥用。 推荐使用IOC容器(例如spring)来管理项目中的单例对象,相对而言,IOC容器内是容器内单例,而单例模式是JVM内单例


results matching ""

    No results matching ""