单例模式是设计模式中最简单的一种, 由于它没有设计模式中各种对象之间的抽象关系, 所以有人不认为它是一种模式, 而是一种实现技巧. 单例模式就像字面的意思一样, 创建全局唯一的一个实例, 提供给外部使用. 要达到这几点要求就要满足三点: 私有构造函数(防止被别人实例化), 静态私有自身对象(用来提供唯一实例), 静态公有的getInstance方法(用来创建和获取唯一实例对象).
优缺点: 单例只允许建立一个唯一实例, 不需要频繁创建和销毁, 可以节省内存加快对象的访问速度.
但是单例没有抽象层和接口, 不方便扩展. 单例既提供工厂方法又提供业务方法, 一定程度上违背了单一职责原则
单例的实现
单例的实现有两种主流方式, 分别是饿汉模式和懒汉模式, 他们在实例化的时机和效率方面各有不同.
饿汉模式
饿汉模式的实现可以参考字面意思. 饿汉, 饥渴的汉子的意思, 要狼吞虎咽抢着吃. 既然单例要求全局只能有一个实例, 那我们通过静态属性直接new出来一个实例. 由于静态属性的初始化是在ClassLoader加载类之后发生的, 而同一个ClassLoader加载同一个类在整个生命周期只会发生一次, 可以保证全局就一个对象实例. 不管业务是否需要用这个实例, 只要应用启动起来了就把唯一实例给创建出来. 所以饿汉模式的特点是空间换时间, 线程安全.
/**
* Created by jesse on 15-6-28.
*/
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {}//一定要有私有构造,要不谈何单例
public static Singleton getInstance(){
return instance;
}
}
懒汉模式
既然单例有两种写法, 那肯定是要补充饿汉模式不合理的地方. 上面分析了饿汉模式的特点是通过空间换时间, 那么就会引出一个问题. 假设我系统里面的单例特别多特别大, 并且有些单例还只是一些不常用的业务在使用, 只使用饿汉模式的话, 会在系统一启动的时候就会狂吃内存降低内存的利用率.
为了提高内存的利用率并节省内存空间, 就有了懒汉模式的写法. 懒汉顾名思义就是一个特别懒的汉子, 既然懒那肯定是什么时候要用单例的实例了再创建单例的对象. 特点是时间换空间, 延时加载.
要用的时候才创建实例对象, 那可不可以用下面代码的写法? 在getInstance时判断对象是否为空, 如果为空说明还没实例过, 才去创建个对象存在静态属性上.
// 懒汉demo 1
public class Singleton {
private static Singleton instance;
private Singleton() {} //一定要有私有构造,要不谈何单例
public static Singleton getInstance() {
if (null == instance) {
// yield
instance = new Singleton();
}
return instance;
}
}
答案当然是不行的. 如果是单线程场景, 上面的写法是OK的. 但是在多线程的情况下:
- 假设线程A运行到
//yield
注释这里时让出CPU时间片. - CPU时间片轮转到线程B, 线程B调用getInstance并创建完对象后让出CPU.
- 线程A继续从
//yield
处运行, 由于上次运行线程A时已经判断过实例为空了, 所以会继续创建对象. - 至此Singleton类被创建了两个对象, 已经违反了单例的唯一性.
所以懒汉模式是线程不安全的, 就算你的应用是单线程的, 这里也强烈不推荐上面的写法, 因为谁也保证不了你的应用以后会不会使用多线程. 为了保证线程安全, 懒汉demo 2里我们给getInstance方法加上锁.
// 懒汉demo 2
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
//给getInstance方法加锁
if (null == instance) {
instance = new Singleton();
}
return instance;
}
}
给getInstance方法加上互斥锁后就就能满足我们的基本要求了. 可是这里有些同学就有额外的要求了: 由于使用锁在多线程并发访问时, 会让一些线程自旋或挂起消耗额外的系统资源, 通常我们会尽量缩小锁的粒度只锁必须要锁的流程, 上面的代码可不可以继续优化呢? 可不可以只锁起来创建对象的部分呢? 我们一起看下面的demo 3.
// 懒汉demo 3
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (null == instance){
synchronized() {
// yield
instance = new Singleton();
}
}
return instance;
}
}
demo 3只锁定了创建对象的部分, 这种写法的问题跟懒汉demo 1有类似的问题:
- 假设线程A申请到锁运行到
//yield
时让出CPU时间片. - CPU轮转到线程B, 线程B调用getInstance. 由于线程A还没有创建对象, 所以线程B阻塞等锁. 让出CPU给线程A.
- 线程A继续从
//yield
处运行, 创建第一个单例实例. 释放锁后让出CPU. - 线程B得到CPU并申请到锁后, 创建第二个单例实例.
- 这个流程还是会创建多个单例对象.
在第四步, 当线程B重复申请到锁的时候instance对象已经有值了, 我们只需要在锁内对单例对象再次判空就可以避免重复得创建对象了. demo 4的代码就是懒汉模式的双检锁写法.
// 懒汉demo 4(双检锁)
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (null == instance) {
synchronized() {
if (null == instance)
instance = new Singleton();
}
}
return instance;
}
}
这下万事大吉了吧, 从代码上挑不出来任何毛病了. 是的, 懒汉demo 4的双检锁写法基本没问题了, 但是从JVM的角度来说还是有很小很小概率会出问题. 这里涉及到对象的创建过程和JVM的指令重排优化.
对象的创建过程大致分为下面五步
- 类加载检查: ClassLoader没有加载过class的话要加载class, 并初始化类的静态成员变量
- 内存分配: 给要创建的对象在堆上分配内存, 并初始化零值.
- 设置对象头: 设置mark word相关信息
- 执行构造方法: 执行构造方法, 初始化业务数据.
- 赋值内存引用: 把对象的内存指给对象的引用.
正常情况下这五步是按顺序执行的. 但是JVM为了提高指令的执行效率, 会对一些非特定的指令进行重新排序. 可能会将第四步和第五步的顺序替换过来.这样就有可能出现下面的情况:
- 懒汉demo 4中, 线程A创建对象触发指令重排. 执行了对象创建过程的1235, 这时instance已经有了对象的引用, 但是对象还没有执行构造方法去初始化一些业务数据. 让出CPU给线程B
- 线程B执行getInstance方法. 由于instance对象已经有值了, 线程B会拿着未完整初始化的对象去使用, 可能会引起一系列奇怪的问题.
那么如何解决JVM的指令重排问题呢? 这里有需要用到java的一个关键字volatile
, 大家可能都知道这个关键字主要是用来做线程间数据可见的, 其实他还有另外一个作用: 禁止指令重排. 使用volatile
修饰的对象, JVM会使用内存屏障来禁止对象相关操作的指令重排. 所以我们还需要再给instance对象加上volatile
关键字修饰. 如下面demo 5, 至此才是真正的万事大吉.
// 懒汉demo 5(最终最完美版)
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (null == instance) {
synchronized() {
if (null == instance)
instance = new Singleton();
}
}
return instance;
}
}
总结
根据上面的分析可以很清楚的了解到: 饿汉模式不用面对对线程并发的问题; 从首次调用速度和响应时间来说饿汉模式要优于懒汉模式; 从资源利用的角度来说懒汉模式要优于饿汉模式; 懒汉模式最好使用双检锁写法.
转载请注明出处:/l2show/article/details/46672061