多线程并发场景下的单例模式设计

单例模式在项目开发中是一种常用的设计模式。对于普通场景下,单例模式分为懒汉单例模式和饿汉单例模式(关于这两种模式的详解参见博文Java设计模式(1)之单例模式学习总结);但是,在现代的软件开发中,多线程、高并发是编写Java应用程序的必须解决的一个问题,如何使单例模式在多线程、高并发的场景下能够正常发挥其作用,是Java程序员必备的知识技能之一;

想要在多线程、高并发的场景中,维持单例模式的单例作用,就必须做到无论多少线程并发调用获取对象实例的方法getInstance()时,都仅会创建一个对象实例,这一点的实现,就得用到Java虚拟机提供的volatile关键字所具有的内存可见性和内存屏障两个特性,来解决多线程并发调用获取实例方法可能带来的重复创建实例对象的问题;

先给出使用volatile关键字的单例模式(DCL单例模式)设计代码:

public class Singleton{
        //使用volatile关键字修饰静态变量singleton
        private volatile static Singleton singleton;

        public static Singleton getInstance(){
            if(singleton == null){
                //使用synchronized代码块进行同步处理
                synchronized(Singleton.class){   //a
                    if(singleton == null){
                        singleton = new Singleton();//b
                    }
                }
            }
            return singleton;
        }
    }

下面对上述代码进行分析:

关于volatile关键字的可见性,可以简单地描述为,如果当前线程在其工作内存中修改了volatile关键字修饰的变量singleton的值,那么,此线程的工作内存中该变量的新值将会被立即刷新到主内存中,此外,其他线程使用变量singleton之前,必须先从主内存中将变量singleton的最新的值读取到线程私有的工作内存中,因此,各个线程使用的singleton变量的值就是最新的值,从而达到在一个线程修改完变量的值之后,修改后的变量值对于其他线程是立即可见的;

关于volatile关键字的内存屏障特性,也就是禁止指令重排序,当某一个线程执行到a处,并获取到对象锁,那么其他的线程将会被阻塞,等待该对象锁的释放,此时获取到对象锁的线程将会进入第二处if语句,这时singleton变量的值为null,因此,执行b处的代码,singleton变量中就存放了刚刚被创建的实例对象的引用;由于变量singleton是volatile关键字修饰的,所以b处的代码经过汇编之后的汇编指令中,在赋值指令之后紧跟着一条带有lock前缀的指令,该指令的作用是使得赋值后变量的新值立即刷新到主内存,从而使得其他线程可以立即观察到该变量的新值,因此,此前的操作指令都已经完成,从而不会被指令重排序到赋值指令之后,起到了内存屏障的作用;

当此线程执行完第二个if语句时,释放对象锁,等待此对象锁的其他线程获取到该锁后,进入if语句,需要在使用singleton变量之前到主内存中获取该变量的最新的值,此时,singleton变量已经不为null,所以,条件不成立,直接返回已创建的实例,也就不会重复创建实例了;
其实,在一个if语句中嵌套一个synchronized同步代码块的用意在于:为了利用volatile关键字的内存可见性和内存屏障,使得那些在b处代码执行之后才执行Singleton.getInstance()的线程可以不用再去无谓地等待获取对象锁,因为变量singleton的值已经不为null了;如果不使用volatile关键字修饰,那么修改后的singleton变量不会立即刷新到主内存,而其他线程也不会看见该变量值的改变,从而使用自己的工作内存中的值为null的变量副本,继而创建多余的对象实例,造成不必要的资源浪费;


猜你喜欢

转载自blog.csdn.net/boker_han/article/details/79401701