Java双重检查式单汉式单例模式&存在的问题&volatile解决

在Java设计模式之单汉式单例模式涉及到多线程访问时,可能用到双重检查式。代码如下:

package com.charles.singleton
public class Singleton
{
    private static Singleton instance=null;
    private Singleton(){}
    public static Singleton getInstance()
    {   
        if(instance==null)  //point 1
        {
            synchronized(Singleton.class){
            if(instance==null)  //point 2
            {
                instance=new Singleton();  //May come into Exception
            }
            }
        }
        return instance;
    }
}

以上代码使得当多个线程同时获取该类对象时,只能持有一个同一个对象的引用。假设同时有线程A以及线程B同时访问,当线程A进入实例之后,进入point 1,此时instance假设为null,满足条件,加锁进入point 2,此时依然满足条件,则实例化instance对象,并返回该对象实例。线程B此时访问,则不满足point 1,直接返回instance对象实例,不会第二次实例化对象。可以看出,双重检查可以使得其执行效率变高,在point 1处,并不会触发synchronized,就不会产生线程的排队等待问题,提高了效率。

其实,这种访问模式是有一定的问题存在,因为在JVM中,存在重排序的问题。在极端情况下,可能会因为重排序问题而导致线程之间的不安全。

重排序:

重排序是JVM中的一种优化代码的运行机制,其特点是语句的原子性。而上述可能引起问题的代码就在

instance=new Singleton();

这句。因为其并非原子性操作,可能会由于重排序(不会引发单线程执行结果的改变)而引起问题。在JVM中,这句话会被分成三步执行,如下:

1.JVM为其分配内存地址以及内存空间

2.使用构造方法实例化对象

3.将分配的内存地址赋予对象

JVM在执行时,如果重排序可能会有以下几种可能:

  执行顺序:1 、2 、3

执行完毕,不出问题。

  执行顺序:1 、 3 、2

执行完毕,产生问题。

产生问题原因,假设线程A刚执行了1,分配了地址以及空间,此时正常。执行3,将分配的地址给对象,正常。此时还未执行2,未实例化该对象,但此时另一个线程B已经到来,由于线程A已经为instance配了地址,instance不为null(但是并没有用构造方法实例化对象,即new Singleton()),返回instance。那么现在线程B拿到的单例对象就不能使用(没有实例化),而产生错误。

解决问题方法:

可以看出,产生这种错误的原因是因为JVM的重排序导致,那么我们可以使用一个关键字来禁止重排序即volatile。

private volatile static Singleton instance=null;

这样,JVM在执行时便不会对其进行重排序而产生错误。

解决问题的原理(volatile):

volatile是通过内存屏障来防止重排序问题

1。在volatile写操作前,插入StoreStore屏障

2。在volatile写操作后,插入StoreLoad屏障

3。在volatile读操作前,插入LoadLoad屏障

4。在volatile读操作后,插入LoadStore屏障

原创文章 42 获赞 72 访问量 8202

猜你喜欢

转载自blog.csdn.net/weixin_43249548/article/details/103713899
今日推荐