多线程下单例模式延迟初始化的实现
前言
在程序开发中,存在一些开销较高的资源,例如数据库连接等,我们使用单例模式保证其唯一并且进行延迟初始化,只有当使用的时候才进行初始化,避免资源的浪。单线程的单例模式的实现较为简单,我们来讲讲在多线程下单例模式的实现
synchronized修饰方法
单例模式延迟初始化的实现最简单的方式莫过于直接使用synchronized修饰方法,但是如果有大量线程同时调用该方法,就会导致大量线程的阻塞在方法外边,只能一个一个的获取实例,严重影响系统性能,我们只需要保证对象的初始化由单个线程执行即可。
双重检查锁定
第一次判断非空:防止线程阻塞,在单例对象进行初始化后直接返回,大大提高了程序性能
第二次判断非空:可能由多个线程阻塞在同步块外,进行非空检验可以避免单例对象的重复创建
双重检查锁定的问题
在上方双重检查锁定的实现中,其实存在问题,问题出现在instance=new Instance(),这一行可以分为三步:
1、分配对象的内存空间
2、初始化对象
3、将instance指向刚分配的内存空间
对于单线程来说,当重排序改变执行的结果时,禁止重排序,而步骤二和步骤三交换并不会改变单线程的执行结果,因此为了优化程序的性能,编译器和处理器可能会对步骤二和步骤三重排序。这就可能导致线程进行步骤一和三之后,另一个形成判断非空,将访问到一个还没有被初始化的对象。
要解决这个问题有两个办法:
1:禁止步骤二和步骤三重排序。
2:允许步骤二和步骤三重排序,不能别的线程"看到"即可
基于volatile的双重锁定方案
对于方法一:我们自然而然的想到了volatile变量,使用volatile修饰单例,volatile写通过加入屏障防止步骤二和步骤三进行重排序。
类初始化
对于方法二:我们采用静态变量的初始化来解决,JVM在类初始化阶段会执行静态变量的初始化,静态变量的初始化只有一次。当线程进行类的初始化时,JVM会获取一个锁。若有多个线程同时需要进行类的初始化,那么每个线程至少都要获取一次锁来确保类是否被初始化,线程初始化一个state变量,用来标记类是否初始化,当线程获取锁时,通过state变量判断是否需要进行初始化。
总结
本文介绍了基于volatile的双重锁定方案和基于类初始化的方案实现多线程下单例模式延迟初始化的实现,通过对比,我们发现类初始化代码更加简洁,但类初始化只适用于静态对象的单例实现,而基于volatile的双重锁定方案使用于静态对象和实例对象的单例实现。
字段延迟初始化降低了单例模式创建对象的开销,但是增加了多线程并发访问对象初始化的开销。大部分情况下正常初始化优于延迟初始化,当需要进行延迟初始化时,若单例对象为静态对象,使用volatile的双重锁定方案实现,若单例对象为实例对象,使用类初始化的方案