我们在之前讲到单例模式的时候,只是关注了如何创建一个单例对象。但其实当时没有关心是否线程安全。简单的单例类会有什么问题呢?先看一下代码。 private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null)
instance = new Singleton();
return instance;
}这是我们所熟悉的单例类。加锁如果只有一个线程顺序地执行当然是没有问题的。但如果有多个线程并发地执行getInstance,可能就会带来很大的问题了。例如,当第一个线程,进入到getInstance中,这时instance变量仍然是null,那么if条件就成立了,这个线程就会去执行创建Singleton实例的那条语句。在第一个线程的赋值语句之前,第二个线程也进入了getInstance方法,这时instance仍然是null,那么第二个线程也会去执行Singleton的构造方法,新建一个对象。这就产生了问题。那为了让这个方法变成线程安全的,我们可以为这个方法加上锁: public static synchronized Singleton getInstance() {
if (instance == null)
instance = new Singleton();
return instance;
}通过使用synchronized关键字,就可以把这个方法变成线程安全的了。但是我们知道其实,这个方法只是在instance为null的时候,会有并发的问题,如果instance已经创建,不为null以后,再调用getInstance()方法就不会再有线程安全的问题了,所以,每次都为这样一个只读的方法加一个锁,性能上划不来。静态内部类在《Effective Java》这本书里,作者推荐了一种叫做静态内部类的办法:class Singleton {
public static Singleton instance;
private static class SingletonWrapper {
static Singleton instance = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
return SingletonWrapper.instance;
}
}
这个代码看上去也很简单,逻辑比较清楚。它是怎么解决了单例类的问题的呢?首先,我们注意到,在第一次调用getInstance方法之前,SingletonWrapper类是没有被加载的,因为它是一个静态内部类。当有线程第一次调用getInstance的时候,SingletonWrapper就会被class loader加载进JVM,在加载的同时,执行instance的初始化。所以,这种写法,仍然是一种懒汉式的单例类。为什么这样写就是线程安全的呢?大家要记住,类的加载的过程是单线程执行的。它的并发安全是由JVM保证的。所以,这样写的好处是在instance初始化的过程中,由JVM的类加载机制保证了线程安全,而在初始化完成以后,不管后面多少次调用getInstance方法都不会再遇到锁的问题了。枚举在Java语言引入了enum关键字以后,我们其实可以使用枚举来实现单例类:public class Main {
public static void main(String[] args) {
Singleton.INSTANCE.sayHello();
}
}
enum Singleton {
INSTANCE;
public void sayHello() {
System.out.println(“hello”);
}
}这个Singleton到底是个什么呢?我们通过javac,将上述代码编译成class文件。然后就会看到,在目标目录下,出现了一个名为Singleton.class的文件,我们把这个文件javap反编译一下(为了节约篇幅,我只贴比较重要的地方):final class Singleton extends java.lang.Enum {
public static final Singleton INSTANCE;
…
static {};
Code:
0: new #4 // class Singleton
3: dup
4: ldc #10 // String INSTANCE
6: iconst_0
7: invokespecial #11 // Method “”:(Ljava/lang/String;I)V
10: putstatic #12 // Field INSTANCE:LSingleton;
13: iconst_1
14: anewarray #4 // class Singleton
17: dup
18: iconst_0
19: getstatic #12 // Field INSTANCE:LSingleton;
22: aastore
23: putstatic #1 // Field $VALUES:[LSingleton;
26: return
}可以看到,enum Singleton只不过就是class Singleton的语法糖而已。在JVM看来,枚举类型不过就是java.lang.Enum类的子类。这个类的static code里说明了在加载Singleton类的时候,就要把instance初始化完成。这仍然利用了类加载器是线程安全的这一特性。不过,从某种意义上说,这种做法有点类似于饿汉式的单例类了。但不管怎么说,enum这个关键字会使得单例类的实现简洁很多,这是目前比较推荐的写法。错误的双重检查关于synchronized,有一种用法,流毒无穷,这就是double check。最早使用double check,是因为synchronized的性能不够好,为了避免不必要的加锁,可以在加锁之前先进行一次判断。具体的写法是这样: public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
看上去很好。但是由于Java的指令重排问题,这种写法其实是有可能出问题的。关键是这一行: instance = new Singleton();这一行包含三个动作,一是申请一块堆内存用于存储Singleton对象,二是执行构造函数,三是将这个对象的地址赋给instance变量。而这三个动作的先后顺序并不是确定的。例如第一个线程先执行了申请内存,将内存地址赋给instance变量,但还未执行构造函数。这时第二个线程进来,然后我们就发现第二个线程将不会进行初始化的动作,直接就拿到了instance对象,但这个时候instance对象是未初始的状态。这就带来了错误。
如果你是Java方向的同学,那么我强烈推荐你进群(374308445)此群致力于分享Java后端技术文章,分享我两年的Java学习心得,以及未来在阿里的点滴。还有几个10年以上开发经验的大牛指导你学习计划。群验证填写【CSDN2】免费领取学习资源(包括Java、C++、前端、移动端、算法、大数据等方向)