单例模式就不在这里详细介绍了,这里要了解的是如何使单例模式遇到多线程是安全的。
单例模式分为饿汉模式和懒汉模式。
饿汉模式就类在加载的时候就创建实例,当使用类的时候已经将对象创建好了
public class SingletonMode1 {
private static SingletonMode1 singletonMode1 = new SingletonMode1();
private SingletonMode1(){}
public static SingletonMode1 getInstance(){
return singletonMode1;
}
public static void main(String[] args){
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Get hashCode: " + SingletonMode1.getInstance().hashCode());
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
Thread thread3 = new Thread(runnable);
Thread thread4 = new Thread(runnable);
Thread thread5 = new Thread(runnable);
thread1.start();
thread2.start();
thread3.start();
thread4.start();
thread5.start();
}
}
运行结果,获取的是同一个对象
懒汉模式是指在使用的时候才去创建对象实例,网上介绍多线程模式下怎样创建线程安全的单例模式很多,这里再回顾下,一步步去构架、完善,分析并解决问题。
最简单的单例模式
public class SingletonMode2 {
private SingletonMode2 mode2 = null;
private SingletonMode2(){}
public static SingletonMode2 getInstance(){
if(mode2 == null){
mode2 = new SingletonMode2();
}
return mode2;
}
}
该写法在单线程情况下没有问题的,那么在多线程模式下回怎样呢?
public class SingletonMode2 {
private static SingletonMode2 mode2 = null;
private SingletonMode2(){}
public static SingletonMode2 getInstance(){
try{
if(mode2 == null){
Thread.sleep(2000); // 模拟生成实例前做一些准备工作
mode2 = new SingletonMode2();
}
} catch (InterruptedException e){
e.printStackTrace();
}
return mode2;
}
public static void main(String[] args){
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(SingletonMode2.getInstance().hashCode());
}
};
Thread threadA = new Thread(runnable);
Thread threadB = new Thread(runnable);
Thread threadC = new Thread(runnable);
Thread threadD = new Thread(runnable);
threadA.start();
threadB.start();
threadC.start();
threadD.start();
}
}
运行结果,完全不一样
比如,当A线程调用getInstance方法的时候,mode2是null,然后进入if条件中,开始为生成对象实例做准备工作,这时mode2还没有去new一个对象,这个时候B线程也开始调用getInstance方法,发现mode2也是null,也开始为生成对象实例做准备工作,同样的后面的C、D线程也一样,都为生成对象实例做准备工作,当他们都做好准备工作之后,然后就都去new一个实例,这就造成获取了不同的实例对象。
知道问题的原因了,那么怎么解决,最简单粗暴的方法,给getInstance方法加锁,试试
public class SingletonMode2 {
private static SingletonMode2 mode2 = null;
private SingletonMode2(){}
public synchronized static SingletonMode2 getInstance(){
try{
if(mode2 == null){
Thread.sleep(2000); // 模拟生成实例前做一些准备工作
mode2 = new SingletonMode2();
}
} catch (InterruptedException e){
e.printStackTrace();
}
return mode2;
}
public static void main(String[] args){
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(SingletonMode2.getInstance().hashCode());
}
};
Thread threadA = new Thread(runnable);
Thread threadB = new Thread(runnable);
Thread threadC = new Thread(runnable);
Thread threadD = new Thread(runnable);
threadA.start();
threadB.start();
threadC.start();
threadD.start();
}
}
运行
是解决了,但是又有一个新的问题,效率问题,也就是每一个来请求的线程都要等上一个线程释放锁之后才能进入获取对象实例,我们的目的只要保证有一个实例就可以,不需要每次都要bala bala一堆操作,那怎么办,对了,为了提供效率,我们可以只给代码块加锁,加了try、catch外面好像没什么提高,那只加了new上,再试试。
public class SingletonMode2 {
private static SingletonMode2 mode2 = null;
private SingletonMode2(){}
public static SingletonMode2 getInstance(){
try{
if(mode2 == null){
Thread.sleep(2000); // 模拟生成实例前做一些准备工作
synchronized (SingletonMode2.class){
mode2 = new SingletonMode2();
}
}
} catch (InterruptedException e){
e.printStackTrace();
}
return mode2;
}
public static void main(String[] args){
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(SingletonMode2.getInstance().hashCode());
}
};
Thread threadA = new Thread(runnable);
Thread threadB = new Thread(runnable);
Thread threadC = new Thread(runnable);
Thread threadD = new Thread(runnable);
threadA.start();
threadB.start();
threadC.start();
threadD.start();
}
}
运行结果
喔!我们还需要在new实例的时候再判断此是不是为空,知道问题后,再改改
public class SingletonMode2 {
private static SingletonMode2 mode2;
private SingletonMode2(){}
public static SingletonMode2 getInstance(){
try{
if(mode2 == null){
Thread.sleep(2000); // 模拟生成实例前做一些准备工作
synchronized (SingletonMode2.class){
if(mode2 == null) {
mode2 = new SingletonMode2();
}
}
}
} catch (InterruptedException e){
e.printStackTrace();
}
return mode2;
}
public static void main(String[] args){
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(SingletonMode2.getInstance().hashCode());
}
};
Thread threadA = new Thread(runnable);
Thread threadB = new Thread(runnable);
Thread threadC = new Thread(runnable);
Thread threadD = new Thread(runnable);
threadA.start();
threadB.start();
threadC.start();
threadD.start();
}
}
运行,哇,实现了。我们在这进行了两次检测,这就是传说中的DCL双重检测锁机制
总算完工,休息下……嗨!!完了吗?好像还有问题吧?考虑过指令重排吗?
额,好像没有……没有。
指令重排就是指java代码编译成JVM指令后,经过CPU和JVM的优化,指令的执行顺序发生变化。
像mode2 = new SingletonMode2();这段代码
编译成JVM指令后分为
1.分配对象内存空间
2.初始化对象
3.为mode2指向刚分配的内存空间
进过指令重排后,1,2,3的顺序可能变成了1,3,2,这时当A线程执行完1和3后,mode2对象还未完成初始化,但是已经不再是null,但是这时如果别的线程抢占了CPU资源,执行(model2 == null)的返回结果是false,然后返回mode2,但是这时的mode2是一个未初始化的对象,额,未初始化的对象,你怎么使用……。
那怎么解决呢,那我们不让指令重排不就行了嘛,这时候你是不是想起了某个关键字啊,没错,就是volatile,它实现了多个线程对变量的可见性,volatile的内存可见性是基于内存屏障实现的,而内存屏障就是一个CPU指令,插入了特定的内存屏障的指令不能重排序(详细的大家可以去看下《深入理解JVM虚拟机》)。
将mode2加上volatile
private volatile static SingletonMode2 mode2;
终于完工了。
当然了,我们还可以使用其他的方式实现单例模式。
静态内部类方法实现
public class SingletonMode3 {
private SingletonMode3(){}
public static SingletonMode3 getInstance(){
return StaticSingletion.INSTANCE;
}
public static void main(String[] args){
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(SingletonMode3.getInstance().hashCode());
}
};
Thread threadA = new Thread(runnable);
Thread threadB = new Thread(runnable);
Thread threadC = new Thread(runnable);
Thread threadD = new Thread(runnable);
threadA.start();
threadB.start();
threadC.start();
threadD.start();
}
public static class StaticSingletion{
private static final SingletonMode3 INSTANCE = new SingletonMode3();
}
}
使用静态内部类同样属于懒汉模式,因为内部类中的INSTANCE是在调用getInstance()方法,StaticSingleton被加载的时候初始化的。