单例模式可能是后端学习者接触到的第一种设计模式,可是单例模式真的有那么简单吗?在并发模式下会出现什么样的问题?在学习了前面的并发知识后,我们来看看究极版的单例模式应该怎么写。
一、单例模式第一版
我们最初接触到的单例模式一般就是懒汉模式与饿汉模式。我们先来看看怎么写:
//懒汉模式
public class Singleton {
private Singleton() {} //私有构造函数
private static Singleton instance = null; //单例对象
//静态工厂方法
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
//饿汉模式
public class Singleton {
private Singleton() {} //私有构造函数
private static Singleton instance = new Singleton(); //单例对象
//静态工厂方法
public static Singleton getInstance() {
return instance;
}
}
要想让一个类只能构建一个对象,自然不能让它随便去做new操作,因此Signleton的构造方法是私有的。
instance是Singleton类的静态成员,也是我们的单例对象。它的初始值可以写成Null,也可以写成new Singleton()。至于其中的区别后来会做解释。
getInstance是获取单例对象的方法。
这两个名字很形象:饿汉主动找食物吃,懒汉躺在地上等着人喂。
1、饿汉式:在程序启动或单件模式类被加载的时候,单件模式实例就已经被创建。
2、懒汉式:当程序第一次访问单件模式实例时才进行创建。
懒汉模式加载快执行慢,但是有线程安全问题,容易引起不同步问题,所以应该创建同步"锁"。
二、单例模式第二版
懒汉模式的线程安全问题主要在if (instance == null)
这句判断是否为空上。在多线程的环境下,可能有多个线程同时通过这个判断。这样一来,就有可能同时创建多个实例。让我们来对代码做一下修改:
public class Singleton {
private Singleton() {} //私有构造函数
private static Singleton instance = null; //单例对象
//静态工厂方法
public static Singleton getInstance() {
if (instance == null) { //双重检测机制
synchronized (Singleton.class){ //同步锁
if (instance == null) { //双重检测机制
instance = new Singleton();
}
}
}
return instance;
}
}
为了防止new Singleton被执行多次,因此在new操作之前加上Synchronized 同步锁,锁住整个类(注意,这里不能使用对象锁)。
进入Synchronized 临界区以后,还要再做一次判空。因为当两个线程同时访问的时候,线程A构建完对象,线程B也已经通过了最初的判空验证,不做第二次判空的话,线程B还是会再次构建instance对象。
然而,这种方法也有一定的缺席。
三、单例模式第三版
假设这样的场景,当两个线程一先一后访问getInstance方法的时候,当A线程正在构建对象,B线程刚刚进入方法。
这种情况表面看似没什么问题,要么Instance还没被线程A构建,线程B执行 if(instance == null)的时候得到true;要么Instance已经被线程A构建完成,线程B执行 if(instance == null)的时候得到false。
我们之前在JAVA并发编程(一):理解volatile关键字学习过指令重排的知识,instance = new Singleton()
这个操作不是一个原子操作,它在执行的时候要经历以下三个步骤:
memory =allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance =memory; //3:设置instance指向刚分配的内存地址
所以这里有可能出现如下情况:
当线程A执行完1,3,时,instance对象还未完成初始化,但已经不再指向null。此时如果线程B抢占到CPU资源,执行 if(instance == null)的结果会是false,从而返回一个没有初始化完成的instance对象。
如何避免这一情况呢?我们需要在instance对象前面增加一个修饰符volatile。
public class Singleton {
private Singleton() {} //私有构造函数
private volatile static Singleton instance = null; //单例对象
//静态工厂方法
public static Singleton getInstance() {
if (instance == null) { //双重检测机制
synchronized (Singleton.class){ //同步锁
if (instance == null) { //双重检测机制
instance = new Singleton();
}
}
}
return instance;
}
}
三、其他方式实现单例模式
实现单例模式的手段还有很多,我们再来看一些别的实现方式。
①静态内部类实现单例模式
public class Singleton {
private static class LazyHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
需要注意的是:
从外部无法访问静态内部类LazyHolder,只有当调用Singleton.getInstance方法的时候,才能得到单例对象INSTANCE。
INSTANCE对象初始化的时机并不是在单例类Singleton被加载的时候,而是在调用getInstance方法,使得静态内部类LazyHolder被加载的时候。因此这种实现方式是利用classloader的加载机制来实现懒加载,并保证构建单例的线程安全。
静态内部类与饿汉&懒汉模式存在共同的问题:无法防止利用反射来重复构建对象。
②枚举实现单例模式
可以防止反射的无懈可击的单例模式代码:
public class SingletonExample {
// 私有构造函数
private SingletonExample() {
}
public static SingletonExample getInstance() {
return Singleton.INSTANCE.getInstance();
}
private enum Singleton {
INSTANCE;
private SingletonExample singleton;
// JVM保证这个方法绝对只调用一次
Singleton() {
singleton = new SingletonExample();
}
public SingletonExample getInstance() {
return singleton;
}
}
}
- 使用枚举实现的单例模式不仅能够防止反射构造对象,而且可以保证线程安全。不过这种方式也有一个缺点,那就是不能实现懒加载,它的单例模式是在枚举类被加载的时候进行初始化的。
参考文章
本文作者: catalinaLi
本文链接: http://catalinali.top/2018/singletonPattern/
版权声明: 原创文章,有问题请评论中留言。非商业转载请注明作者及出处。