单例模式:双重检查+synchronized关键字+volitle关键字

本文介绍了Java中的单例模式实现,重点探讨了双重检查、synchronized关键字和volatile关键字在单例模式中的作用。通过实例分析了在并发环境下,volatile保证了可见性和一定程度的有序性,但不保证原子性,而synchronized则确保了有序性和原子性,结合使用以确保单例的安全性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

实现单例模式,有3个要点:

  1. 某个类只能有一个实例;
  2. 这个实例只能本类自己创建;
  3. 创建的这个实例必须向整个系统开放。

为了满足这3个要点,单例模式必须:

  1. 在本类中实例化;
  2. 构造器必须私有,外界不能通过调用构造器创建对象;
  3. 必须对外提供一个静态的方法供外界获取该类的实例。

简单的一个单例模式:双重检查 + synchronized关键字 + volitle关键字如下:

public class Singleton {

    private static volatile Singleton singleton = null;

    private Singleton() {}

    public static Singleton getSingleton() {

        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

这里面有3个关键点:

1. 用到了关键字volatile;
2. 用到了关键字synchronized;
3. 用到了双重检查if (singleton == null)

来来来,我们给这个单例模式加点料,来分析下,注意在 #0 中,去掉了 volatile

package com.example;

public class Singleton {

    // 去掉了volatile关键字
    private static Singleton singleton = null; //#0

    public static int count_0 = 0;
    public static int count_1 = 0;
    public static int count_2 = 0;
    public static int count_3 = 0;
    public static int count_3_1 = 0;

    private Singleton() {
    }

    public static Singleton getSingleton() {
        count_0++;
        if (singleton == null) { // #1
            count_1++;
            synchronized (Singleton.class) { // #2
                count_2++;
                if (singleton == null) { // #3
                    count_3++;
                    singleton = new Singleton(); // #4
                    System.err.println(Thread.class.getName() + "Singleton is null, it is being instantiated"); // #5
                } else {
                    count_3_1++;
                    System.err.println(Thread.class.getName() + "Singleton is not null, because it has been instantiated"); // #6
                }
            }
        }
        return singleton;
    }
}

测试类:

package com.example;

public class SingletonTest {

    public static void main(String[] args) throws InterruptedException {
        int count = 0;
        for (int i = 0; i < 5000; i++) {
            count++;
            new Thread(new SingleThread(), "thread" + i + 1).start();
        }

        Thread.sleep(2000);
        System.err.println("count:" + count);

        System.err.println("count_0: " + Singleton.count_0);
        System.err.println("count_1: " + Singleton.count_1);
        System.err.println("count_2: " + Singleton.count_2);
        System.err.println("count_3: " + Singleton.count_3);
        System.err.println("count_3_1: " + Singleton.count_3_1);
    }

    static class SingleThread implements Runnable {

        @Override
        public void run() {
            Singleton.getSingleton();
        }
    }
}

运行结果:

java.lang.ThreadSingleton is null, it is being instantiated
java.lang.ThreadSingleton is not null, because it has been instantiated
java.lang.ThreadSingleton is not null, because it has been instantiated
count:5000
count_0: 4992
count_1: 3
count_2: 3
count_3: 1
count_3_1: 2

运行的结果并不唯一,现在以上面的结果分析下,
#1处检测Singleton实例是否非空,从count_1 = 3,可以看出在这5000个线程中,有3个线程在获取Singleton实例时,实例还没有创建。在#2处,对这个类加锁synchronized,有1个线程获得了锁执行#2后面的代码,另外2个被阻塞在#2外面。获得锁的这个线程于是走过#3 #4 #5 创建了Singleton实例,然后释放了锁。剩下的2个线程中的1个此时获得了锁,另1个依旧阻塞,获得锁的在#3处检测Singleton实例是否非空,由于前面一个线程已经创建了实例,此时来到了#6处,释放锁;最后1个线程也一样。

一切看起来很正常,平常我们也是这么处理单例模式的,双重检查 + synchronized关键字,那为啥又有个volitle关键字出现呢?

其实上面的程序运行,还可能出现类似这样的情况:

java.lang.ThreadSingleton is null, it is being instantiated
java.lang.ThreadSingleton is null, it is being instantiated
java.lang.ThreadSingleton is not null, because it has been instantiated

也即Singleton对象被实例化两(多)次,虽然我试着运行好几分钟,也没有出现这种情况,但是理论上是会出现的,为什么?
注意到count_0这个变量,开启了5000个线程,理论上应该和count的结果是一样5000。而实际的结果值却是4992。我们知道成员变量存在堆内存中,静态成员变量存在于方法区中(java.8以后使用了元空间(meta space)来实现jvm的方法区定义),而堆内存和方法区都是共享的,能被多线程访问,这就涉及到多线程下的安全性问题了
而并发访问涉及到三个重要的概念:原子性、可见性、有序性

原子性:在一个或多个操作中,要么全部执行且执行的过程不会被打断,要么全部不执行;
可见性:在多线程访问共享变量时,一个线程对变量的修改,其他的线程会立即感知;
有序性:程序的执行顺序是按照代码的顺序执行。

需要知道一点:在并发访问下,必须同时满足上面三个条件才行,缺一不可

于count_0++,这个自增动作:

  1. 不是原子性操作。包括读取变量,赋值,写入等动作。这又涉及到了java内存模型Java Memorry Model(JMM)这里就不深入了。
  2. 也不具备可见性。譬如count_0的原值是0线程1读到count_0的值为0,并执行 ++动作,结果为1,还没来得及更新到主内存;此时线程2也读到了count_0的值为0,并没有立即读到线程1执行的结果,结果也为1。最后两个线程执行后更新到主内存的count_0的值是1,这就导致上面的count_0count结果不一致的原因了。

同理,#0处的静态成员变量singleton也是线程不安全的。

Singleton singleton = new Singleton();

不具备原子性和可见性

那volatile关键字就具备了原子性、可见性和有序性么?
我们先看下加上后的运行结果,修改代码,在变量count_0前面加上关键字volatile

public static volatile int count_0 = 0;

运行结果:

java.lang.ThreadSingleton is null, it is being instantiated
java.lang.ThreadSingleton is not null, because it has been instantiated
count:5000
count_0: 4998
count_1: 2
count_2: 2
count_3: 1
count_3_1: 1

我们发现count_0的结果依旧不等于count的值,也就是说还是线程不安全的。那么问题来了,在单例模式的双重检查中为啥还需要个volitle关键字呢?这里只给出它的作用:能保证可见性和一定的有序性,但是不能保证原子性。根据这个结论,我们看看加了volitle关键字的单例双重检查 + synchronized关键字模式。

 private static volatile Singleton singleton = null; //#0

volitle能保证可见性,也就是说只要一个线程对共享变量修改了,其他线程都能立即看到。有序性也是一定程度的,哪“不一定的程度”怎么保证呢?还有原子性怎么保证?这里别忘了,还有个synchronized关键字。synchronized是啥,是排他锁、同步锁,也就是说同一时刻只会有一个线程获得锁,其他的都在等待锁的释放。所以,synchronized就保证了有序性和原子性

PScount_0singleton都是共享变量,从上面的运行结果看count_0出现不安全的概率有(5000-4998)/5000(很小),而5000个线程中只有几个进入了#1后,所以确实很不容易出现。

所以记得,在写单例模式时,用双重检查 + synchronized关键字 + voliltle关键字,这样才能保证绝对的安全。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值