熟悉Java的朋友应该对单例设计模式了解的不少,比如我们会发现Spring中默认交给容器管理的对象都是单例的,除非你手动的设置。那么Spring是如何保证容器中的Bean对象只创建一次呢?下面,我们就分析一下单设计及模式与并发碰撞在一起会出现怎样的火花。
懒汉模式和饿汉模式得区别:
懒汉模式就是懒,不用它就不创建,等到要用的时候才创建(赶晚不赶早)
饿汉模式就是饿死鬼,还没到用到得时候他就创建出来,就像还没有到吃饭的时间,就早早把饭做好了。(赶早不赶晚)。
一.懒汉模式
1.懒汉设计模式(一挡!)。
/**
* 懒汉模式(一挡)
*
*/
@Slf4j
@UnThreadSafe//这两个注解你不需要关心
public class test_Singleton_lazy{
//获取实例对象
@Test
public void fun1(){
/**
* 下面的意思就是开50个线程,并发的获取Singleton_Lazy的实例
*
*/
ExecutorService executorService = Executors.newCachedThreadPool();
for(int i= 0 ; i< 50 ; i++){
executorService.execute(()->{
System.out.println("singleton_lazy1的HashCode为:"+Singleton_Lazy.getSingletion().hashCode());
});
}
}
}
//最基本的单例设计模式,也是我们最先接触到的,在单线称下可以正确的运行,但是高并发下,就会出错!
class Singleton_Lazy {
//构造函数私有化
private Singleton_Lazy(){
}
//创建成员变量
private static Singleton_Lazy singleton_lazy=null;
//静态方法获取目标对象
public static Singleton_Lazy getSingletion(){
if(singleton_lazy == null){
singleton_lazy = new Singleton_Lazy();
}
return singleton_lazy;
}
}
高并发下的测试结果:
解释:上面我们开了50个线程同时获取Singleton_Lazy的实例,我们可以模拟一下线程。当一条线程通过下面的判断条件进入if块后,在还没来得及改变singleton_lazy的值或线程工作区虽然改变了但是还未及时刷新回内存的时候,另一个线程也通过了if判断,进入了if语句块内部,这时,后一个线程和前一个线程将持有不同的返回对象,单例模式被高并发破环!
if(singleton_lazy == null){
singleton_lazy = new Singleton_Lazy();
}
下面我们对上面的懒汉设计模式进行升级!
2.懒汉设计模式(二挡!)
/**
*
* 单例设计模式,懒汉式(二挡)
*/
@Slf4j
@UnThreadSafe
public class test_Singleton_lazy_01 {
@Test
public void fun1(){
/**
* 下面的意思就是开50个线程,并发的获取Singleton_Lazy的实例
*
*/
ExecutorService executorService = Executors.newCachedThreadPool();
for(int i= 0 ; i< 50 ; i++){
executorService.execute(()->{
System.out.println("singleton_lazy1的HashCode为:"+Singleton_lazy_01.getSingleton_lazy_01().hashCode());
});
}
}
}
class Singleton_lazy_01{
//构造方法私有化
private Singleton_lazy_01(){
}
private static Singleton_lazy_01 singleton_lazy_01 = null ;
/**
*
* 锁定静态方法=锁定类对象
* @return
*/
public synchronized static Singleton_lazy_01 getSingleton_lazy_01(){
if(singleton_lazy_01 == null){
singleton_lazy_01 = new Singleton_lazy_01();
}
return singleton_lazy_01;
}
}
synchronized修饰的方法,线程只能串行执行,对该方法来说,和单线程一样了,所以可以解决并发问题,但是我们一般不会直接锁方法,因为一般方法中都会有大量的业务逻辑,如果直接将在方法上加锁,那么锁的粒度将会异常的大,这样太伤害程序的执行效率了。
3.懒汉设计模式(三挡!)
/**
*
* 单例设计模式,懒汉式(二挡)
*/
@Slf4j
@UnThreadSafe
public class test_Singleton_lazy_01 {
@Test
public void fun1(){
/**
* 下面的意思就是开50个线程,并发的获取Singleton_Lazy的实例
*
*/
ExecutorService executorService = Executors.newCachedThreadPool();
for(int i= 0 ; i< 50 ; i++){
executorService.execute(()->{
System.out.println("singleton_lazy1的HashCode为:"+Singleton_lazy_01.getSingleton_lazy_01().hashCode());
});
}
}
}
class Singleton_lazy_01{
//构造方法私有化
private Singleton_lazy_01(){
}
private static Singleton_lazy_01 singleton_lazy_01 = null ;
public static Singleton_lazy_01 getSingleton_lazy_01(){
/**
* 说明几点:
* 1.因为是静态方法,我们需要对当前类对象来上锁,以保证原子性。
* 2.使用双重if判断+synchronized来保证原子性。(DoubleCheck)
*
*/
if(singleton_lazy_01 == null){
synchronized (Singleton_lazy_01.class){
if(singleton_lazy_01 == null){
singleton_lazy_01 = new Singleton_lazy_01();
}
}
}
return singleton_lazy_01;
}
}
高并发下的测试结果:
我先来解释一下为什么这样写把!
if(singleton_lazy_01 == null){
synchronized (Singleton_lazy_01.class){
if(singleton_lazy_01 == null){
singleton_lazy_01 = new Singleton_lazy_01();
}
}
}
解释:第一层循环,判断当前的singleton_lazy_01是不是null,如果是,则进入if语句块,如果不是证明已经创建过对象了,直接返回就好 如果有多个线程同时通过了第一层判断,那么它们就会在synchronized语句块外等待(堆积等待),即使已经有线程给getSingleton_lazy_01赋值了,但是这些线程依然会根据cup的调度顺序,依次进入synchronized语句块。所以我们需要在synchronized语句块中再加一层if判断, 不然每一个通过了第一层判断的线程都会新创建一个对象,这就破坏了单例模式。
感觉好像说的很有道理,感觉好像问题已经得到了解决,但是真的是这样嘛?不是的!上面这Singleton_lazy_01依然不能算的上是单例的,下面,我们就来解释一下,为什么。
指令从排序将会破环经典的DoubleCheck。
熟悉操作系统和JVM的朋友可能都知道,我们的CPU会进行乱序优化,我们的JVM在执行某段代码的时候,可能会将不符合Happens-before原则的指令进行乱序优化,我们的编译器有时也会进行代码的乱序优化。
有了上面的乱序优化(指令重排序)的知识,我们就来分析分析,上面的Singleton_lazy_01 究竟有什么问题。
singleton_lazy_01 = new Singleton_lazy_01();
这段代码,在执行时并不是原子性的,对应在我们的.class文件的字节码指令上应该有三个(对应到cpu上也许会有更多个)
1.分配给新创建的对象内存(堆区)空间。
2.初始化新创建对象(属性赋值呀,依赖注入啊什么什么的)。
3.将singleton_lazy_01的引用指向内存中(堆区)新创建的对象。
看起来好像很和谐,但是如果在乱序优化后,将cup指令变化为下面这个顺顺序:
1.分配给新创建的对象内存(堆区)空间。
3.将singleton_lazy_01的引用指向内存中(堆区)新创建的对象。
2.初始化新创建对象(属性赋值呀,依赖注入啊什么什么的)。
那么就会出现一个问题。当一个线程执行到第3步但未执行第2步时,如果此时一个线程使用if判断时,那么singleton_lazy_01将不为空,直接返回了一个未被初始化的对象(构造为完成就发生了逃逸),虽然这种情况出现的机率很小,但是单例设计模式显然已经被破坏,这个错误在平时的开发中很容易被忽略,导致一些意想不到的错误,希望大家注意。
4.懒汉设计模式(四挡!)
class Singleton_lazy_01{
//构造方法私有化
private Singleton_lazy_01(){
}
private static volatile Singleton_lazy_01 singleton_lazy_01 = null ;
public static Singleton_lazy_01 getSingleton_lazy_01(){
if(singleton_lazy_01 == null){
synchronized (Singleton_lazy_01.class){
if(singleton_lazy_01 == null){
singleton_lazy_01 = new Singleton_lazy_01();
}
}
}
return singleton_lazy_01;
}
}
使用volatile修饰我们的成员变量singleton_lazy_01,在这里,volatile起到的作用是使用内存屏障(barrier),防止在volatile修饰的变量在执行时被JVM或cpu进行乱序优化。
如果想要知道volatile是如何使用内存屏障保证竟态条件的可见性于有序性(字节码层面)的话,我给大家推荐一篇博客,很不错哦:
在这里顺便提一句,volatile一共就两个比较常见的用处:
1.修饰状态标识量。(如private static volatile boolean flag=false;)
2.双重检测(Double check),也就是我们上面实例中的双重if,用于禁止指令重排序。
二.饿汉模式
首先需要说明,静态属性,静态代码块都会在类加载机制的初始化阶段被收集到<clinit>()(类构造器方法中)。虚拟机会保证一个类的<clinit>()会被正确的加锁,同步如果多个线程去调用这个类的<clinit>()方法,那么只会有一个线程执行该方法,其它线程都将被阻塞,直到活动线程执行<clinit>()方法完毕。
1.饿汉模式(一挡!)
@ThreadSafe
public class Singleton_Hungry {
@Test
public void fun1(){
/**
* 下面的意思就是开50个线程,并发的获取Singleton_Lazy的实例
*
*/
ExecutorService executorService = Executors.newCachedThreadPool();
for(int i= 0 ; i< 50 ; i++){
executorService.execute(()->{
System.out.println("singleton_lazy1的HashCode为:"+testSingletonHungry.getTestSingletonHungry().hashCode());
});
}
}
}
class testSingletonHungry{
//构造器私有化
private testSingletonHungry(){
}
//直解在类加载的时候就创建实例对象
private static testSingletonHungry hungry = new testSingletonHungry();
//获取实例化对象
public static testSingletonHungry getTestSingletonHungry(){
return hungry;
}
}
这里使用静态域实现初始化,静态域属性在类加载的时候就会被初始化到方法区,类只会被加载一次,所以可以保证单例。唯一性由JVM保证。
二.饿汉模式(二挡!)
class testSingletonHungry{
//构造器私有化
private testSingletonHungry(){
}
//直解在类加载的时候就创建实例对象
private static testSingletonHungry hungry = null;
static {
hungry = new testSingletonHungry();
}
//获取实例化对象
public static testSingletonHungry getTestSingletonHungry(){
return hungry;
}
}
这里只是将上面的静态域实现初始化变成了使用静态代码块实现初始化,不过要注意了,使用这种方式时需要注意,静态代码块需要在静态域属性之后,否则返回的实例对象是空的。唯一性也是由JVM保证。
3.饿汉模式(三挡!)
class testSingletonHungry{
//构造器私有化
private testSingletonHungry(){
}
//获取实例化对象
public static testSingletonHungry getTestSingletonHungry(){
return Enum_for_Singleton.INSTANCE.getTestSingletonHungry();
}
//定义枚举类
private enum Enum_for_Singleton{
INSTANCE;
private testSingletonHungry hungry;
//JVM保证该方法只会被调用一次,而且是绝对的!所以可以保证单例!
Enum_for_Singleton(){
hungry = new testSingletonHungry();
}
//返回创建的实例
public testSingletonHungry getTestSingletonHungry(){
return hungry;
}
}
}
使用一个内部的枚举类,完成了对目标对象的生成与单例的控制,相对于其它饿汉模式,该方式虽然是饿汉模式,但是目标对象的创建会在首次调用时创建,而不是在类加载时创建,减轻了类加载的负担,提高了类加载的速度。而较懒汉模式来说,并发性更容易得到保证,更加安全。所以,我们比较推荐使用这种方式!
至此,这篇小记也就完结了,如有不妥之处,还望海涵!谢谢。