双重检查单例,为什么需要加volatile才可以?

先来看看懒汉模式,所谓懒汉式就是: 一开始不初始化示例对象,当我们需要这个对象,调用getInstance然后再初始化对象


class ClassB {

    private volatile static ClassB INSTANCE;

    public  static ClassB getInstance() {
        if(INSTANCE==null){
              INSTANCE=new ClassB();
        }
        return INSTANCE;
    }
}

很明显这是一个线程不安全的单例,当两个线程同时进入if(INSTANCE==null)成立,此时就会出现创建出了两个对象. 因此线程不安全,不信的话可以来测试一下.

public class DCLSingletonTest {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(100);
        for (int i = 0; i < 10000; i++) {
            pool.submit(() -> {
//                ClassA instance = ClassA.getInstance();
                ClassB instance = ClassB.getInstance();
                System.out.println(Thread.currentThread().getName() + " : " + instance.hashCode());
            });
        }
        pool.shutdown();
    }
}

出现问题

pool-1-thread-2 : 612342154
pool-1-thread-1 : 1394206243
pool-1-thread-3 : 612342154
pool-1-thread-4 : 612342154
pool-1-thread-5 : 612342154
.....

ok,如果需要改成线程安全的单例,可以直接在方法上加上synchronized 关键字(如下),但是这样一来性能就会下降,synchronized 属于重锁

   public synchronized static ClassB getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new ClassB();
        }
        return INSTANCE;
    }

因此我们可以缩小锁的粒度(如下所示),天真的你以为这样就线程安全了吗???, 其实和上面一样,如果此时多个线程同时进行if (INSTANCE == null) 都进入了if里,此时虽然只有一个线程进入synchronized代码块内,但是当这个线程执行出了代码块释放了锁,下个线程拿到锁,任然会创建两个对象.

  public static ClassB getInstance() {
        if (INSTANCE == null) {
            synchronized (ClassB.class){
                INSTANCE = new ClassB();
            }
        }
        return INSTANCE;
 }

因此,双重检查来了, 和上面一样,此时第二个线程进入同步代码块内,会进行一次if判断,此时 INSTANCE已经第一个线程创建了对象,并且把引用赋值给了它,因此第二个线程if不通过,不会再次的创建对象.

  public static ClassB getInstance() {
        if (INSTANCE == null) {
            synchronized (ClassB.class){
                if(INSTANCE==null){
                    INSTANCE = new ClassB();
                }
            }
        }
        return INSTANCE;
    }

你以为这就完了吗???? 重头戏来了.. (INSTANCE需要用volatile关键字修饰吗?)

答案: yes

class ClassB {

    private /*volatile*/ static ClassB INSTANCE;

    public static ClassB getInstance() {
        if (INSTANCE == null) {
            synchronized (ClassB.class){
                if(INSTANCE==null){
                    INSTANCE = new ClassB();
                }
            }
        }
        return INSTANCE;
    }
}

为什么呢???
举个栗子

class A{
	int a=9;
}

首先要了解对象创建的步骤 ,即 INSTANCE = new ClassB();

第一步: 使用new运算符,先申请内存空间
第二步: 给创建的对象赋初始值 (a=0)
第三步: 给对象赋值(保存调用Object的 . 以及本来的这两个方法) (a=9)
第四步: 将此对象的引用赋值给 INSTANCE

在此过程中如果发生重排序的话: 就是这几个步骤可能乱序,因此可能第一个线程创建对象的使用,先赋默认值,
随后就将此对象的引用赋值给了INSTANCE, 很不幸,此时第二个线程来了,判断if (INSTANCE == null)成立,随后就返回了这个INSTANCE . 但是此时的 INSTANCE 的属性的值还是默认值.

如何解决呢?
volatile修饰 INSTANCE即可
volatile的作用

  • 保证线程可见性 (利用MESI缓存一致性协议)
  • 禁止指令重排序
  • 不保证原子性
# Java   设计模式   单例  

评论

公众号:mumuser

企鹅群:932154986

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×