实现单例模式,有3个要点:
- 某个类只能有一个实例;
- 这个实例只能本类自己创建;
- 创建的这个实例必须向整个系统开放。
为了满足这3个要点,单例模式必须:
- 在本类中实例化;
- 构造器必须私有,外界不能通过调用构造器创建对象;
- 必须对外提供一个静态的方法供外界获取该类的实例。
简单的一个单例模式:双重检查 + synchronized关键字 + volitle关键字如下:
public class Singleton {
private static volatile Singleton singleton = null;
private Singleton() {}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
这里面有3个关键点:
1. 用到了关键字volatile;
2. 用到了关键字synchronized;
3. 用到了双重检查if (singleton == null)
来来来,我们给这个单例模式加点料,来分析下,注意在 #0 中,去掉了 volatile:
package com.example;
public class Singleton {
// 去掉了volatile关键字
private static Singleton singleton = null; //#0
public static int count_0 = 0;
public static int count_1 = 0;
public static int count_2 = 0;
public static int count_3 = 0;
public static int count_3_1 = 0;
private Singleton() {
}
public static Singleton getSingleton() {
count_0++;
if (singleton == null) { // #1
count_1++;
synchronized (Singleton.class) { // #2
count_2++;
if (singleton == null) { // #3
count_3++;
singleton = new Singleton(); // #4
System.err.println(Thread.class.getName() + "Singleton is null, it is being instantiated"); // #5
} else {
count_3_1++;
System.err.println(Thread.class.getName() + "Singleton is not null, because it has been instantiated"); // #6
}
}
}
return singleton;
}
}
测试类:
package com.example;
public class SingletonTest {
public static void main(String[] args) throws InterruptedException {
int count = 0;
for (int i = 0; i < 5000; i++) {
count++;
new Thread(new SingleThread(), "thread" + i + 1).start();
}
Thread.sleep(2000);
System.err.println("count:" + count);
System.err.println("count_0: " + Singleton.count_0);
System.err.println("count_1: " + Singleton.count_1);
System.err.println("count_2: " + Singleton.count_2);
System.err.println("count_3: " + Singleton.count_3);
System.err.println("count_3_1: " + Singleton.count_3_1);
}
static class SingleThread implements Runnable {
@Override
public void run() {
Singleton.getSingleton();
}
}
}
运行结果:
java.lang.ThreadSingleton is null, it is being instantiated
java.lang.ThreadSingleton is not null, because it has been instantiated
java.lang.ThreadSingleton is not null, because it has been instantiated
count:5000
count_0: 4992
count_1: 3
count_2: 3
count_3: 1
count_3_1: 2
运行的结果并不唯一,现在以上面的结果分析下,
#1处检测Singleton实例是否非空,从count_1 = 3,可以看出在这5000个线程中,有3个线程在获取Singleton实例时,实例还没有创建。在#2处,对这个类加锁synchronized,有1个线程获得了锁执行#2后面的代码,另外2个被阻塞在#2外面。获得锁的这个线程于是走过#3 #4 #5 创建了Singleton实例,然后释放了锁。剩下的2个线程中的1个此时获得了锁,另1个依旧阻塞,获得锁的在#3处检测Singleton实例是否非空,由于前面一个线程已经创建了实例,此时来到了#6处,释放锁;最后1个线程也一样。
一切看起来很正常,平常我们也是这么处理单例模式的,双重检查 + synchronized关键字,那为啥又有个volitle关键字出现呢?
其实上面的程序运行,还可能出现类似这样的情况:
java.lang.ThreadSingleton is null, it is being instantiated
java.lang.ThreadSingleton is null, it is being instantiated
java.lang.ThreadSingleton is not null, because it has been instantiated
也即Singleton对象被实例化两(多)次,虽然我试着运行好几分钟,也没有出现这种情况,但是理论上是会出现的,为什么?
注意到count_0这个变量,开启了5000个线程,理论上应该和count的结果是一样5000。而实际的结果值却是4992。我们知道成员变量存在堆内存中,静态成员变量存在于方法区中(java.8以后使用了元空间(meta space)来实现jvm的方法区定义),而堆内存和方法区都是共享的,能被多线程访问,这就涉及到多线程下的安全性问题了。
而并发访问涉及到三个重要的概念:原子性、可见性、有序性。
原子性:在一个或多个操作中,要么全部执行且执行的过程不会被打断,要么全部不执行;
可见性:在多线程访问共享变量时,一个线程对变量的修改,其他的线程会立即感知;
有序性:程序的执行顺序是按照代码的顺序执行。
需要知道一点:在并发访问下,必须同时满足上面三个条件才行,缺一不可。
对于count_0++,这个自增动作:
- 不是原子性操作。包括读取变量,赋值,写入等动作。这又涉及到了java内存模型Java Memorry Model(JMM)这里就不深入了。
- 也不具备可见性。譬如count_0的原值是0,线程1读到count_0的值为0,并执行 ++动作,结果为1,还没来得及更新到主内存;此时线程2也读到了count_0的值为0,并没有立即读到线程1执行的结果,结果也为1。最后两个线程执行后更新到主内存的count_0的值是1,这就导致上面的count_0和count结果不一致的原因了。
同理,#0处的静态成员变量singleton也是线程不安全的。
Singleton singleton = new Singleton();
不具备原子性和可见性。
那volatile关键字就具备了原子性、可见性和有序性么?
我们先看下加上后的运行结果,修改代码,在变量count_0前面加上关键字volatile:
public static volatile int count_0 = 0;
运行结果:
java.lang.ThreadSingleton is null, it is being instantiated
java.lang.ThreadSingleton is not null, because it has been instantiated
count:5000
count_0: 4998
count_1: 2
count_2: 2
count_3: 1
count_3_1: 1
我们发现count_0的结果依旧不等于count的值,也就是说还是线程不安全的。那么问题来了,在单例模式的双重检查中为啥还需要个volitle关键字呢?这里只给出它的作用:能保证可见性和一定的有序性,但是不能保证原子性。根据这个结论,我们看看加了volitle关键字的单例双重检查 + synchronized关键字模式。
private static volatile Singleton singleton = null; //#0
volitle能保证可见性,也就是说只要一个线程对共享变量修改了,其他线程都能立即看到。有序性也是一定程度的,哪“不一定的程度”怎么保证呢?还有原子性怎么保证?这里别忘了,还有个synchronized关键字。synchronized是啥,是排他锁、同步锁,也就是说同一时刻只会有一个线程获得锁,其他的都在等待锁的释放。所以,synchronized就保证了有序性和原子性。
PS:count_0和singleton都是共享变量,从上面的运行结果看count_0出现不安全的概率有(5000-4998)/5000(很小),而5000个线程中只有几个进入了#1后,所以确实很不容易出现。
所以记得,在写单例模式时,用双重检查 + synchronized关键字 + voliltle关键字,这样才能保证绝对的安全。