最近想重新回顾下设计模式,特此记录
一、概述
单例模式可以说是在开发中用得最多的一种模式了。以此来作为第一个回顾的模式。
单例模式简单来说分为懒汉式与饿汉式。
饿汉式
先看代码:
public class Singleton{
private final static Singleton mSingleton = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return mSingleton;
}
}
优点:这种写法比较简单,在类装载的时候就已经完成了实例化,属于线程安全;
缺点:不管需不需要,对象都存在,如果未使用,会造成内存浪费。
懒汉式
顾名思义,这种方式为Lazy Loading。只有在需要的时候才初始化实例。
懒汉式有多中写法,让我们来看看这种
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
咋一看,没啥毛病啊,singleton对象为null 才创建新的对象。确实,在没有高并发的情况下,这种写法没问题。但是当高并发存在时,假如一个线程A进入判断if(singleton == null) 为true时,准备创建新的对象但还未创建完成时(创建对象也是需要时间的),另一个线程B也进入到了if(singleton == null) 的判断,由于此时对象还未创建完成,所以AB两个线程会创建两个对象。
此种写法,线程不安全!
那么加上synchronized 关键字不就解决了吗?
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
线程安全完美解决。但是仔细一想,是不是有很大的弊端。
缺点:效率太低,每个线程想要获取实例的时候,都需要进行同步。
修改修改…
双重检查锁定
public class Singleton {
private static Singleton singleton;
private Singleton () {}
public static Singleton getInstance() {
//这次判空保证的多线程只有 singleton== null 时候才会加锁初始化,提高效率
if (singleton== null) {
synchronized (Singleton .class) {
//这次判断保证高并发下,代码块加锁后只执行一次实例化
if (singleton== null) {
singleton= new Singleton ();
}
}
}
return singleton;
}
}
看起来很完美了?保证了对象的唯一性,大幅降低了synchronized 带来的性能开销。一切只是看起来很完美。
指令重排
根据《The Java Language Specification, Java SE 7 Edition》(简称为java语言规范),
所有线程在执行java程序时必须要遵守 intra-thread semantics(译为 线程内语义是一个单线程程序的基本语义)。
intra-thread semantics 保证重排序不会改变单线程内的程序执行结果。
换句话来说,intra-thread semantics 允许那些在单线程内,不会改变单线程程序执行结果的重排序。
singleton= new Singleton () 在JVM中做了什么操作呢?
memory = allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
singleton= memory; //3:设置instance指向刚分配的内存地址
根据 intra-thread semantics 上述步骤中2 与 3 是有可能重排序的
Time | Thread A | Thread B |
---|---|---|
t1 | A1:分配对象的内存空间 | |
t2 | A3:设置instance指向刚分配的内存地址 | |
t3 | B1:判断singleton == null | |
t4 | B2: singleton 不为null ,将访问singleton引用的对象 | |
t5 | A2:初始化对象 | |
t6 | A4:访问singleton对象 |
在此种情况下,线程B访问到的是一个没有初始化的对象
造成这种情况的唯一根源就是指令的重排
怎么解决这种情况呢。只需要做一点小修改,将声明对象的引用为volatile后即可,这样可以在多线程环境下禁止指令的重排
修改后方案
public class Singleton {
private volatile static Singleton singleton;
private Singleton () {}
public static Singleton getInstance() {
//这次判空保证的多线程只有 singleton== null 时候才会加锁初始化,提高效率
if (singleton== null) {
synchronized (Singleton .class) {
//这次判断保证高并发下,代码块加锁后只执行一次实例化
if (singleton== null) {
singleton= new Singleton ();
}
}
}
return singleton;
}
}
注意,这个解决方案需要JDK5或更高版本(因为从JDK5开始使用新的JSR-133内存模型规范,这个规范增强了volatile的语义)。
归根结底,单例模式需要考虑的就是线程安全与高性能,那么还有没有其他方式?
/**推荐**/
public class Singleton{
private Singleton() {}
static class SingletonHolder {
private static final Singleton singleton= new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.singleton;
}
}
由于对象实例化是在内部类加载的时候构建的,因此该版是线程安全的(因为在方法中创建对象,才存在并发问题,静态内部类随着方法调用而被加载,只加载一次,不存在并发问题,所以是线程安全的)。
另外,在getInstance()方法中没有使用synchronized关键字,因此没有造成多余的性能损耗。当Singleton类加载的时候,其静态内部类SingletonHolder并没有被加载,因此singleton对象并没有构建。
而我们在调用Singleton.getInstance()方法时,内部类SingletonHolder被加载,此时单例对象才被构建。因此,这种写法节约空间,达到懒加载的目的,该版也是众多博客中的推荐版本。