第二个设计模式我们来一起学习单例模式。应该说只要知道设计模式的,必然知道单例模式,这几乎是设计模式的一个入门必备。可是是不是所有人都清楚java应该怎么实现单例模式最为安全?我们听说过的懒汉方式、饿汉方式、double-check方式、内部静态类方式、枚举方式,这几种实现方式究竟怎么回事?孰优孰劣?下面我们就一起学习一下。
- 懒汉方式
/**
* 懒汉方式
*
* @author josh
*/
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (lazySingleton == null) {
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
- 分析上面代码,懒汉方式的优点是获取实例的时候去判断实例是否初始化了,如果实例是null,则初始化返回,也就是说在第一次使用的时候才去创建实例,这样能够节省内存空间。这种方式适用于单例使用的次数少,并且创建单例消耗的资源较多,单例按需创建。
- 缺点可以一眼看出,在线程安全方面并没有做任何考虑,多线程的情况下调用getInstance方法时,就可能创建多个线程实例,因此需要加锁解决线程同步问题:
改造后的饿汉方式:
/**
* 懒汉方式
*
* @author josh
*/
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton() {
}
public static synchronized LazySingleton getInstance() {
if (lazySingleton == null) {
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
- 饿汉方式
/**
* 饿汉方式
*
* @author josh
*/
public class HungrySingleton {
private static HungrySingleton singleton = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return singleton;
}
}
- 饿汉方式代码简洁、易懂,写起来很容易。他的好处就是在单例类加载的时候就创建了实例,因此不存在线程安全的问题,实例在整个生命周期都存在。它适用于单例初始化时占用内存比较小的情况,如果初始化时占用内存比较大或初始化后特定场景才用到单例对象,这种方式就不太适用了。
- 它的缺点也很明显了,就是类加载时就实例化对象了,它存在于整个生命周期,因此会一直占用内存直至jvm被终止。
- Double Check方式【推荐使用】
/**
* 双重校验锁
*
* @author josh
*/
public class DoubleCheckSingleton {
/**
* 防止java指令重排优化
*/
private volatile static DoubleCheckSingleton uniqueInstance;
/**
* 构造方法私有化
*/
private DoubleCheckSingleton() {
}
/**
* 双重校验 获取实例
*
* @return
*/
public static DoubleCheckSingleton getInstance() {
//第一次检查
if (uniqueInstance == null) {
synchronized (DoubleCheckSingleton.class) {
//第二次检查
if (uniqueInstance == null) {
uniqueInstance = new DoubleCheckSingleton();
}
}
}
return uniqueInstance;
}
}
- 大部分情况下,代码在第一次检查执行完毕后就可以i返回结果了,从而提升了性能。
- 考虑多线程情况下的初始化,当A、B两个线程同时执行到第一次检查时,两个线程都判断为空,代码进入同步代码块,A、B线程依次执行同步代码块中的内容,因此如果没有第二次检查,此刻就会创建两个实例出来。
- 代码中开始部分定义成员变量时,使用了volatile关键字(JDK1.5以后才增加的关键字),并注释说明防止java指令重排优化,那么什么是java的指令重排优化呢? 所谓指令重排优化是指JVM在不改变原语义的情况下,通过调整指令的执行顺序让程序运行的更快。如果没有使用volatile关键字,就会导致初始化单例对象和将对象地址赋值给uniqeuInstance字段的顺序是不确定的。例如A线程在创建单线程的实例时,在构造方法执行前,JVM就为实例对象分配了内存空间,并将对象的成员变量赋予默认值,此时就可以将分配的内存地址赋值给uniqueInstance了,而此时若有B线程来调用getInstance,取到的就是状态有问题的对象,程序就会报错。因此需要使用volatile来修饰成员变量避免此类问题。
- 内部静态类方式【推荐使用】
/**
* 静态内部类
*
* @author josh
*/
public class InnerClassSingleton {
private static class Singleton {
public static InnerClassSingleton singleton = new InnerClassSingleton();
}
private InnerClassSingleton() {
}
public InnerClassSingleton getInstance() {
return Singleton.singleton;
}
}
- 观察源码,这种方式实现其实也是很简便的。这种方式利用了类的加载机制,JVM加载内部类的时候创建对象,因此不存在线程安全的问题,跟饿汉模式有相似之处。
- 与饿汉模式的区别就是,它在一个内部类中创建的实例对象,这样的只有使用到这个内部类时JVM才去加载,然后初始化实例对象,也就实现了饿汉模式的延迟加载。这种方式既保证了延迟加载,又保证了线程安全。
- 枚举方式
/**
* 枚举方式
*
* @author josh
*/
public class EnumSingleton {
private EnumSingleton() {
}
public static EnumSingleton getInstance() {
return SingletonEnum.SINGLETON_ENUM.getInstance();
}
private enum SingletonEnum {
//定义枚举值
SINGLETON_ENUM;
private EnumSingleton singleton;
private SingletonEnum() {
singleton = new EnumSingleton();
}
public EnumSingleton getInstance() {
return singleton;
}
}
}
- 枚举的构造方法被私有化了,getInstance方法访问枚举时会执行构造方法;枚举的实例(SINGLETON_ENUM)都是static final修饰的,也就是枚举被加载的时候实例化一次,因此EnumSingleton类也被实例化一次。
- 之前的四种方法都具备同样的缺点,就是第一需要额外的工作来实现序列化;第二,可以通过JDK提供的发射机制调用私有构造器来实例化多个对象。枚举类很好的解决了上面四种方式的共同缺点, 使用枚举除了线程安全和防止反射调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。
总结以上5种模式的单例实现方式: