一、前言
如何使单例模式遇到多线程是安全的、正确的?
我们在学习设计模式的时候知道单例模式有懒汉式和饿汉式之分。简单来说,饿汉式就是在使用类的时候已经将对象创建完毕,懒汉式就是在真正调用的时候进行实例化操作。
二、饿汉式+多线程
单例:
public class MyObject {
//饿汉模式
private static MyObject myObject=new MyObject();
private MyObject(){
}
public static MyObject getInstance(){
return myObject;
}
}
自定义线程:
public class MyThread extends Thread {
@Override
public void run(){
System.out.println(MyObject.getInstance().hashCode());
}
}
main方法:
public class Run {
public static void main(String[] args) {
MyThread t1=new MyThread();
MyThread t2=new MyThread();
MyThread t3=new MyThread();
t1.start();
t2.start();
t3.start();
}
}
结果:
hashCode是同一个值,说明对象是同一个。也就是说饿汉式单例模式在多线程环境下是线程安全的。
三、懒汉式+多线程
方案一:
单例:
public class MyObject {
private static MyObject myObject;
/*私有构造函数避免被实例化*/
private MyObject(){
}
public static MyObject getInstance(){
try {
if (myObject==null) {
//模拟在创建对象之前做的一些准备性工作
Thread.sleep(3000);
myObject=new MyObject();
}
} catch (Exception e) {
e.printStackTrace();
}
return myObject;
}
}
自定义线程:
public class MyThread extends Thread {
@Override
public void run(){
System.out.println(MyObject.getInstance().hashCode());
}
}
main方法:
public class Run {
public static void main(String[] args) {
MyThread t1=new MyThread();
MyThread t2=new MyThread();
MyThread t3=new MyThread();
t1.start();
t2.start();
t3.start();
}
}
结果:
3种hashCode,说明创建出了3个对象,并不是单例的。懒汉模式在多线程环境下是“非线程安全”。这是为何?
因为创建实例对象的那部分代码没有加synchronized或Lock。三个线程都进入了创建实例对象的代码段getInstance。
方案二:synchronized同步方法
既然多个线程可以同时进入getInstance()方法,那么只需要对getInstance()方法声明synchronized关键字即可。在MyObject的getInstance()方法前加synchronized关键字。最终打印的三个hashcode是一样一样的。实现了多线程环境下,懒汉模式的正确性、安全性。但是此种方法的运行效率非常低下,因为是同步的,一个线程释放锁之后,下一个线程继续执行。
方案三:synchronized同步代码块
同步方法是对方法整体加锁,效率不高,我们可以通过减少锁的粒度,也就是使用synchronized同步代码块。如下面代码所示:
public class MyObject {
private static MyObject myObject;
/*私有构造函数避免被实例化*/
private MyObject(){
}
/*synchronized*/
public static MyObject getInstance(){
try {
synchronized (MyObject.class) {
if (myObject==null) {
//模拟在创建对象之前做的一些准备性工作
Thread.sleep(3000);
myObject=new MyObject();
}
}
} catch (Exception e) {
e.printStackTrace();
}
return myObject;
}
}
这样做能保证最终运行结果正确,但getInstance方法中的全部代码都是同步的了,这样做会降低运行效率,和对getInstance方法加synchronized的效率几乎一样。
方案四:重要代码同步代码块
public class MyObject {
private static MyObject myObject;
/*私有构造函数避免被实例化*/
private MyObject(){
}
/*synchronized*/
public static MyObject getInstance(){
try {
if (myObject==null) {
//模拟在创建对象之前做的一些准备性工作
Thread.sleep(3000);
synchronized (MyObject.class) {
myObject=new MyObject();
}
}
} catch (Exception e) {
e.printStackTrace();
}
return myObject;
}
}
结果:
这种做法在多线程环境下还是无法解决得到同一个实例对象的结果。
方案五:双重锁定
package singleton_3;
public class MyObject {
private static MyObject myObject;
/*私有构造函数避免被实例化*/
private MyObject(){
}
//使用双重锁定(Double-Check Locking)解决问题,既保证了不需要同步代码的异步执行性,
//又保证了单例的效果
public static MyObject getInstance(){
try {
if (myObject==null) {
//模拟在创建对象之前做一些准备性的工作
Thread.sleep(3000);
synchronized(MyObject.class){
if (myObject==null) {
myObject=new MyObject();
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return myObject;
}
}
使用双重锁定功能,成功地解决了在多线程环境下“懒汉模式”的“非线程安全”问题。
那么为什么外面已经判断myObject实例是否存在,为什么在lock里面还需要做一次myObject实例是否存在的判断呢?
如果myObject已经存在,则直接返回,这没有问题。当Instance为null,并且同时有3个线程调用GetInstance()方法时,它们都可以通过第一重myObject==null的判断,然后由于lock机制,这三个线程只有一个进入,另外2个在外排队等候,必须第一个线程走完同步代码块之后,第二个线程才进入同步代码块,此时判断instance==null,为false,直接返回myObject实例。就不会再创建新的实例啦。第二个监测myObject==null一定要在同步代码块中。
方案六:
方案五表面上来看,在执行该代码时,先判断instance对象是否为空,为空时再进行初始化对象。即使是在多线程环境下,因为使用了synchronized锁进行代码同步,该方法也仅仅创建一个实例对象。但是,从根本上来说,这样写还是存在一定问题的。 问题源头:
创建对象:1.创建对象时限分配内存空间-----》2.初始化对象-----》3.设置对象指向内存空间-----》4.初次访问对象;
2和3可能存在重排序问题,由于单线程中遵守intra-thread semantics,从而能保证即使2和3交换顺序后其最终结果不变。但是当在多线程情况下,线程B将看到一个还没有被初始化的对象,此时将会出现问题。
解决方案:
1、不允许②和③进行重排序
2、允许②和③进行重排序,但排序之后,不允许其他线程看到。
基于volatile的解决方案
对前面的双重锁实现的延迟初始化方案进行如下修改:
public class MyObject {
private volatile static MyObject myObject;
/* 私有构造函数避免被实例化 */
private MyObject() {
}
/* synchronized */
public static MyObject getInstance() {
try {
if (myObject == null) {
// 模拟在创建对象之前做的一些准备性工作
Thread.sleep(3000);
synchronized (MyObject.class) {
if (myObject == null) {
myObject = new MyObject(); // 用volatile修饰,不会再出现重排序
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return myObject;
}
}
使用volatile修饰instance之后,之前的②和③之间的重排序将在多线程环境下被禁止,从而保证了线程安全执行。
注意:这个解决方案需要JDK5或更高版本(因为从JDK5开始使用新的JSR-133内存模型规范,这个规范增强了volatile的语义)
基于类初始化的解决方案
JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。基于这个特性,可以实现另一种线程安全的延迟初始化方案。
public class MyObject {
private static MyObject myObject;
/*私有构造函数避免被实例化*/
private MyObject(){
}
//静态内部类方式
private static class MyObjectHandler{
private static MyObject myObject=new MyObject();
}
public static MyObject getInstance(){
return MyObjectHandler.myObject;
}
}
结果可行。
使用静态代码块实现单例模式
public class MyObject {
private static MyObject instance;
/*私有构造函数避免被实例化*/
private MyObject(){
}
static{
instance=new MyObject();
}
public static MyObject getInstance(){
return instance;
}
}
结果可行。 该方案的实质是,允许②和③进行重排序,但不允许非构造线程(此处是B线程)“看到”这个重排序。
四、总结
单例模式分为懒汉式和饿汉式,饿汉式在多线程环境下是线程安全的;懒汉式在多线程环境下 是“非线程安全”的,可以通过synchronized同步方法和“双重检测”机制来保证懒汉式在多线程环境下的线程安全性。静态内部类实现单例模式和静态代码块从广义上说都是饿汉式的。