目录
0 什么是单例模式
1 饿汉法
2 懒汉法
3 双重校验锁
3.1 volatile关键字的作用及原理
4 枚举法 (Effective Java推荐)
5 总结
本文旨在学习总结不同的单例模式写法,并做优缺点分析。
在《Design Patterns:Elements of Resuable Object-Oriented Software》中的定义是:Ensure a class only has one instance,and provide a global point of access to。它的主要特点不是根据客户程序调用生成一个新的实例,而是控制某个类型的实例数量——唯一一个(《设计模式-基于C#的工程化实现及扩展》,王翔)。也就是说,单例模式就是保证在整个应用程序的生命周期中,在任何时刻,被指定的类只有一个实例,并为客户程序提供一个获取该实例的全局访问点。
在第一次引用该类的时候就创建对象。在类加载的时候就已经创建好了一个静态的对象Singleton供系统使用,以后不再改变,所以它是线程安全的,避免了多线程同步的问题。
缺点:即使单例没有用到,也会被创建,浪费内存资源。
public class Singleton {
private static Singleton = new Singleton();
private Singleton() {}
public static getSignleton(){
return singleton;
}
}
懒汉法解决了饿汉法浪费资源的问题,在需要的时候才去创建对象。
缺点:线程不安全。 在多个线程可能会并发调用它的getInstance()方法,导致创建多个实例。
竞态条件会导致singleton引用被多次赋值,使用户得到两个不同的单例。
public class Singleton {
private static Singleton singleton = null;
private Singleton(){}
public static Singleton getSingleton() {
if(singleton == null) // 并发场景下,这里存在竞态条件
{ singleton = new Singleton();}
return singleton;
}
}
考虑线程安全和效率,修改以下3点。这种方法之所以成为是双重校验锁,就是因为在getSingleton() 方法中,进行了两次null的检查。
public class Singleton {
private static volatile Singleton singleton = null; //1.volatile保证对线程的可见性并禁止指令重排优化
private Singleton(){}
public static Singleton getSingleton(){
if(singleton == null){ //2.提高效率,减少synchronized加锁操作
synchronized (Singleton.class){ //3.对象锁,阻塞
if(singleton == null){
singleton = new Singleton(); //不加volatile关键字多线程情况下用户会拿到半个对象
}
}
}
return singleton;
}
}
双重校验锁方法修改的2、3点很好理解。那么第1点,如果没有volatile 关键字,会出现什么问题呢?
我们先来看volatile关键字的作用:(1)保持内存可见性;(2)防止指令重排。
内存可见性:所有线程都能看到共享内存的最新状态。
我们来看这样一个可变整数类:
public class MutableInteger {
private int value;
public int get(){
return value;
}
public void set(int value){
this.value = value;
}
}
类MutableInteger 不是线程安全的,在多线程的情况下,如果线程1调用了set方法,那么正在调用get的线程2 可能看到更新后的值,也可能看不到。解决方法就是,将value 声明成 volatile 变量。
private volatile int value;
防止指令重排:在Java中看似顺序的代码在JVM中,可能会出现编译器或者CPU对这些操作指令进行了重新排序。
接下来,我们来分析一下,如果没有volatile 关键字,会出现什么问题呢?
问题出在这行简单的赋值语句:
singleton = new Singleton();
它并不是一个原子操作。事实上,它可以”抽象“为下面几条JVM指令:
memory = allocate(); //1:分配对象的内存空间
initInstance(memory); //2:初始化对象
singleton = memory; //3:设置singleton指向刚分配的内存地址
操作2依赖于操作1,但是操作3并不依赖于操作2,所以JVM可以以“优化”为目的对它们进行重排序,经过重排序后如下:
memory = allocate(); //1:分配对象的内存空间
singleton = memory; //3:设置singleton指向刚分配的内存地址(此时对象还未初始化)
ctorInstance(memory); //2:初始化对象
可以看到指令重排之后,操作 3 排在了操作 2 之前,即引用 singleton 指向内存 memory 时,这段崭新的内存还没有初始化——即,引用singleton指向了一个"被部分初始化的对象"。此时,如果另一个线程调用getInstance方法,由于singleton已经指向了一块内存空间,从而if条件判为false,方法返回singleton 引用,用户得到了没有完成初始化的“半个”单例对象。
所以,解决方法,就是将singleton 声明为volatile变量。
但是,上面提到的所有实现方式都有两个共同的缺点:
public enum Singleton {
INSTANCE;
private String name;
public String getName(){
return name;
}
public void setName(String name){
this.name = name;
}
}
无论以上哪种方法都需要考虑的是:一个核心原理就是私有构造,并且通过静态方法获取一个实例。
在这个过程中还需要考虑:
以上?