单例模式
Singleton
确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。 单例模式最主要的特点:
- 构造函数不对外开放,一般为private;
- 通过一个静态方法或者枚举返回单例类对象;
- 确保单例类的对象有且只有一个,尤其是在多线程的环境下;
- 确保单例类对象在反序列化时不会重新构建对象。
使用要点 线程安全; 延迟加载; 序列化与反序列化安全。
单例分类
饿汉式
|
|
- 单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。 缺点是它不是一种懒加载模式(lazy initialization)。
懒汉式
|
|
使用了懒加载模式,但是线程不安全。
|
|
最简单的方法是将整个 getInstance() 方法设为同步(synchronized)。 虽然做到了线程安全,并且解决了多实例的问题,但是它并不高效。 但是同步操作只需要在第一次调用时才被需要,即第一次创建单例实例对象时。这就引出了双重检验锁。
双重检验锁
|
|
如果没有volatile
就会出现多个实例。
主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。
- 给 instance 分配内存
- 调用 Singleton 的构造函数来初始化成员变量
- 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了) 但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的。如果一个线程第三步先执行完毕,2还未执行,其它线程以为instance为非null,直接返回instance就报错了。 在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。 java中volatile关键字作用 当volatile用于一个作用域时,Java保证如下: (适用于Java所有版本)读和写一个volatile变量有全局的排序。也就是说每个线程访问一个volatile作用域时会在继续执行之前读取它的当前值,而不是(可能)使用一个缓存的值。(但是并不保证经常读写volatile作用域时读和写的相对顺序,也就是说通常这并不是有用的线程构建)。 (适用于Java5及其之后的版本)volatile的读和写建立了一个happens-before关系,类似于申请和释放一个互斥锁。
注意在 Java 5 以前的版本使用了 volatile 的双检锁还是有问题的。其原因是 Java 5 以前的 JMM (Java 内存模型)是存在缺陷的,即时将变量声明成 volatile 也不能完全避免重排序
静态内部类 static nested class
|
|
使用JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;
枚举 Enum
|
|
使用枚举除了线程安全和防止反射强行调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。 通过EasySingleton.INSTANCE来访问实例。创建枚举默认就是线程安全的,所以不需要担心double checked locking,而且还能防止反序列化导致重新创建新的对象。
总结
一般来说,单例模式有五种写法:懒汉、饿汉、双重检验锁、静态内部类、枚举。 一般情况下直接使用饿汉式就好了,如果明确要求要懒加载(lazy initialization)会倾向于使用静态内部类,如果涉及到反序列化创建对象时会试着使用枚举的方式来实现单例。