经典单例模式
定义两个线程t1,t2去获得类的实例,然后输出实例名字。我们加入count 从 1000000递减的操作是为了增加getInstance()的执行时间,使得观察出想要的结果。
/**
* @author eventime
*
* 单例模式与并发编程
*/
public class SInstance {
private static SInstance instance;
public static SInstance getInstance() {
if (instance == null) {
int count = 1000000;
while (count -- > 0);
instance = new SInstance();
}
return instance;
}
public static void main(String[] args) {
Thread t1 = new Thread(()-> System.out.println(SInstance.getInstance()));
Thread t2 = new Thread(()-> System.out.println(SInstance.getInstance()));
t1.start();
t2.start();
}
}
输出结果:
SInstance@653be230
SInstance@7ddc7898
不难理解这里输出了两个不同的 instance。这是因为两个线程可能同时进入这段代码,当判断instance为空的时候,都会去尝试新建一个实例。最终导致输出结果错误。
if (instance == null) {
int count = 1000000;
while (count -- > 0);
instance = new SInstance();
}
加锁的单例模式
最简单的解决办法就是使用synchronized给调用方法加上一个锁。这样得到的结果便是正确的,因为同一时间只能有同一个线程在创建实例。当实例被创建后,锁才会被释放。因此其他线程访问来的时候看到的instance并不是null。
/**
* @author eventime
*
* 单例模式与并发编程
*/
public class SInstance {
private static SInstance instance;
public static synchronized SInstance getInstance() {
if (instance == null) {
int count = 1000000;
while (count -- > 0);
instance = new SInstance();
}
return instance;
}
public static void main(String[] args) {
Thread t1 = new Thread(()-> System.out.println(SInstance.getInstance()));
Thread t2 = new Thread(()-> System.out.println(SInstance.getInstance()));
t1.start();
t2.start();
}
}
实验结果:
SInstance@53765f6c
SInstance@53765f6c
但是简单在方法上加锁会大大增加调用方法的性能损耗。我们可以使用下面的方式来改善性能。
- 方法加锁**:
**synchronized**
方法会锁住整个方法,即每次调用该方法时,其他线程必须等待,哪怕只是在检查条件时(如是否需要创建实例),这可能会导致不必要的等待。** - 代码块加锁:
**synchronized (SInstance.class)**
** 仅锁住特定的代码块,这样可以避免对整个方法加锁,从而减少锁的范围,提高性能**
性能优化后的加锁单例模式(双重锁)
选择在方法块上加锁,值得注意的是这里出现了两次判断 instance是否为空因此该方法被命名为双重锁。
因为在第一次判断出instance为null的时候,可能有多个进程进入第一个判断为空的代码块。但是只有一个线程能够进入加锁的代码块来新建instance。新建完成后,线程释放锁。当在等待得到锁的线程需要再判断依次instance是否为空,因为可能已经有线程创建了instance。
/**
* @author eventime
*
* 单例模式与并发编程
*/
public class SInstance {
private static SInstance instance;
public static SInstance getInstance() {
if (instance == null) {
int count = 1000000;
while (count -- > 0);
synchronized (SInstance.class) {
if (instance == null) {
instance = new SInstance();
}
}
}
return instance;
}
public static void main(String[] args) {
Thread t1 = new Thread(()-> System.out.println(SInstance.getInstance()));
Thread t2 = new Thread(()-> System.out.println(SInstance.getInstance()));
t1.start();
t2.start();
}
}
代码在逻辑上看上去没有问题的,但是在多线程的环境下还是可能出现问题。
应当注意到 instance = new SInstance(); 并非原子操作,具体来说它有三个不同的原子操作组成。
- 分配内存**:为
**SInstance**
对象分配内存。** - 初始化对象**:初始化分配的内存,包括设置对象的字段和调用构造函数。**
- 设置引用:将对象的引用分配给
**instance**
变量。
而三个原子操作可能会引发** 重排序问题 **
如果原子操作被重排序为:
- 分配内存:为
**SInstance**
对象分配内存。 - 设置引用:将对象的引用分配给
**instance**
变量。 - 初始化对象:初始化分配的内存,包括设置对象的字段和调用构造函数。
如果在进行到第二步的时候,引用已经设置,但是初始化尚未完成。此时如果有线程来判断instance是否为空,得到的结果将是否,随后返回的可能是未完成初始化的instance。
线程安全的双重锁
解决上面问题的方法便是在instance定义前加入volatile关键字。它将禁止在涉及instance的操作时,使用重排序。值得注意的是,重排序可能发生在编译在字节码阶段,jvm生成native code阶段,以及硬件执行阶段。
/**
* @author eventime
*
* 单例模式与并发编程
*/
public class SInstance {
private static volatile SInstance instance;
public static SInstance getInstance() {
if (instance == null) {
int count = 1000000;
while (count -- > 0);
synchronized (SInstance.class) {
if (instance == null) {
instance = new SInstance();
}
}
}
return instance;
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()-> System.out.println(SInstance.getInstance()));
Thread t2 = new Thread(()-> System.out.println(SInstance.getInstance()));
t1.start();
t2.start();
}
}