单例模式大概有以下几种写法:
1.饿汉模式
2.懒汉模式
3.double-check
4.枚举
5.静态内部类
分别从这几种方式来看线程安全问题。
1.饿汉模式(2种写法)
根据初始化时机的不同分为两种写法
1.1 声明+初始化(线程安全)
在声明的时候直接初始化,可以保证它是线程安全的,因为instance被声明为static,属于类,而类加载时就会创建instance实例,不论线程如何访问,都只会获取到该实例。
缺点:有句话叫“是盔甲也是软肋”,在类加载时就创建实例虽然能保证线程安全,但如果初始化过程十分复杂会导致类加载的时间变长,并且如果这个实例不会被用到,就浪费了资源。
public class Singleton{
private Singleton(){}
private static Singleton instance = new Singleton();
public static Singleton getInstance(){
return instance;
}
}
1.2 静态块初始化(线程安全)
我们也可以在静态块中对实例初始化,不过要注意的是static instance的声明应当在static块之前,否则会出现instance对象为null。
public class Singleton{
private Singleton(){}
//单例对象
private static Singleton instance = null;
static{
instance = new Singleton();
}
//静态工厂方法
public static Singleton getInstance(){
return instance;
}
}
2.懒汉模式(2种)
2.1线程不安全的写法
懒汉模式是在第一次调用静态工厂方法时创建实例,为什么说它是线程不安全的呢?
首先在单线程的情况下是没有任何问题的,现在假如有两个线程A和B同时访问getInstance方法,都判定instance为空,这时候就会创建出不同的实例。
public class Singleton{
private Singleton(){}
private static Singleton instance = null;
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
2.2线程安全的写法
使用synchronized修饰静态方法,可以确保同一时刻只有一个线程可以访问方法,做到了线程安全,但是会带来性能开销。
public class Singleton{
private Singleton(){}
private static Singleton instance = null;
public static synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
3.double-check(2种)
3.1线程不安全的double-check(因为指令重排)
双重检查,根据懒汉模式稍加改动
双重检查是指,在静态工厂方法getInstance中,先执行一次instance的检查,如果instance为空,则使用synchronized关键字锁住这个类,可以保证synchronized作用范围内只有一个实例被创建。
public class Singleton{
private Singleton(){}
private static Singleton instance = null;
public static Singleton getInstance(){
if(instance == null){ //double - check // B
synchronized(Singleton.class){
if(instance == null){ //A -> 3
instance = new Singleton();
}
}
}
return instance;
}
}
但为什么说他是线程不安全的呢?标题已经剧透啦,是因为指令重排。具体来看一下是怎样的情况。
首先对于这条语句
instance = new Singleton();
会被CPU转换为下面的指令 :
1.memory = allocate() 分配内存空间
2.ctorInstance() 初始化对象
3.instance = memory 设置instance指向刚刚分配的内存
对于2和3,因为没有什么必要的先后顺序(根据happens-before原则无法推导出它的顺序),因此可能会被重排序,也许会排成下面的样子:
1.memory = allocate() 分配内存空间
3.instance = memory 设置instance指向刚刚分配的内存
2.ctorInstance() 初始化对象
对照上面的程序,如果此时有两个线程A和B,当A已经执行到标识的位置,并且已经执行了上面第3条将instance指向分配好的内存,此时B执行判断,发现instance不为空,那么直接return instance了,可是这时候instance还没初始化呢,就被B线程发布了,我们说这里发生了对象逸出,因此它是不安全的。
如何能保证安全呢?看下边一个写法。
3.2 线程安全的double-check(禁止指令重排)
既然上面说,是因为指令重排导致了不安全,那就不要指令重排了,也许你就会想到,是不是用volatile关键字来禁止指令重排。
看下实现,非常简单就是给instance对象添加volatile关键字。
volatile阻止的不是上面说的3个指令的重排,而是保证了在一个写操作(instance = new Singletone() // A)完成之前,不会调用读操作(if (instance == null) //B)。
public class Singleton{
private Singleton(){}
//使用volatile禁止重排序
private volatile static Singleton instance = null;
public static Singleton getInstance(){
if(instance == null){ //double - check // B
synchronized(Singleton.class){
if(instance == null){ //A
instance = new Singleton();
}
}
}
return instance;
}
}
4.枚举类(线程安全)
JVM保证枚举类的构造函数只被调用一次
相比懒汉,在安全方面更容易保证
相比饿汉,在调用时才会初始化
public class Singleton{
private Singleton(){}
public static Singleton getInstance(){
return Inner.INSTANCE.getInstance();
}
private enum Inner{
INSTANCE;
private Singleton singleton;
Inner(){
singleton = new Singleton();
}
public Singleton getInstance(){
return singleton;
}
}
}
5.静态内部类(线程安全)
public class Singleton{
private Singleton(){}
public static Singleton getInstance(){
return StaticSingleton.instance;
}
private static class StaticSingleton{
private static final Singleton instance = new Singleton();
}
}
写的话一般写5或者4就可以了。
(这是一个学习笔记总结贴,课程《Java并发编程入门与高并发面试》,来自慕课网的jimin老师,讲的超好推荐大家去看)