JAVA多线程基础:单例模式与双重检查锁

经典单例模式

定义两个线程t1,t2去获得类的实例,然后输出实例名字。我们加入count 从 1000000递减的操作是为了增加getInstance()的执行时间,使得观察出想要的结果。

/**
 * @author eventime
 *
 * 单例模式与并发编程
 */
public class SInstance {
    private static SInstance instance;
    public static SInstance getInstance() {
        if (instance == null) {
            int count = 1000000;
            while (count -- > 0);
            instance = new SInstance();
        }
        return instance;
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(()-> System.out.println(SInstance.getInstance()));
        Thread t2 = new Thread(()-> System.out.println(SInstance.getInstance()));
        t1.start();
        t2.start();
    }
}

输出结果:

SInstance@653be230

SInstance@7ddc7898

不难理解这里输出了两个不同的 instance。这是因为两个线程可能同时进入这段代码,当判断instance为空的时候,都会去尝试新建一个实例。最终导致输出结果错误。

if (instance == null) {
    int count = 1000000;
    while (count -- > 0);
    instance = new SInstance();
}

加锁的单例模式

最简单的解决办法就是使用synchronized给调用方法加上一个锁。这样得到的结果便是正确的,因为同一时间只能有同一个线程在创建实例。当实例被创建后,锁才会被释放。因此其他线程访问来的时候看到的instance并不是null。

/**
 * @author eventime
 *
 * 单例模式与并发编程
 */
public class SInstance {
    private static SInstance instance;
    public static synchronized SInstance getInstance() {
        if (instance == null) {
            int count = 1000000;
            while (count -- > 0);
            instance = new SInstance();
        }
        return instance;
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(()-> System.out.println(SInstance.getInstance()));
        Thread t2 = new Thread(()-> System.out.println(SInstance.getInstance()));
        t1.start();
        t2.start();
    }
}

实验结果:

SInstance@53765f6c

SInstance@53765f6c

但是简单在方法上加锁会大大增加调用方法的性能损耗。我们可以使用下面的方式来改善性能。

  • 方法加锁**:**synchronized** 方法会锁住整个方法,即每次调用该方法时,其他线程必须等待,哪怕只是在检查条件时(如是否需要创建实例),这可能会导致不必要的等待。**
  • 代码块加锁:**synchronized (SInstance.class)**** 仅锁住特定的代码块,这样可以避免对整个方法加锁,从而减少锁的范围,提高性能**

性能优化后的加锁单例模式(双重锁)

选择在方法块上加锁,值得注意的是这里出现了两次判断 instance是否为空因此该方法被命名为双重锁。

因为在第一次判断出instance为null的时候,可能有多个进程进入第一个判断为空的代码块。但是只有一个线程能够进入加锁的代码块来新建instance。新建完成后,线程释放锁。当在等待得到锁的线程需要再判断依次instance是否为空,因为可能已经有线程创建了instance。

/**
 * @author eventime
 *
 * 单例模式与并发编程
 */
public class SInstance {
    private static SInstance instance;
    public static SInstance getInstance() {
        if (instance == null) {
            int count = 1000000;
            while (count -- > 0);
            synchronized (SInstance.class) {
                if (instance == null) {
                    instance = new SInstance();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(()-> System.out.println(SInstance.getInstance()));
        Thread t2 = new Thread(()-> System.out.println(SInstance.getInstance()));
        t1.start();
        t2.start();
    }
}

代码在逻辑上看上去没有问题的,但是在多线程的环境下还是可能出现问题。

应当注意到 instance = new SInstance(); 并非原子操作,具体来说它有三个不同的原子操作组成。

  • 分配内存**:为 **SInstance** 对象分配内存。**
  • 初始化对象**:初始化分配的内存,包括设置对象的字段和调用构造函数。**
  • 设置引用:将对象的引用分配给 **instance** 变量。

而三个原子操作可能会引发** 重排序问题 **

如果原子操作被重排序为:

  • 分配内存:为 **SInstance** 对象分配内存。
  • 设置引用:将对象的引用分配给 **instance** 变量。
  • 初始化对象:初始化分配的内存,包括设置对象的字段和调用构造函数。

如果在进行到第二步的时候,引用已经设置,但是初始化尚未完成。此时如果有线程来判断instance是否为空,得到的结果将是否,随后返回的可能是未完成初始化的instance。

线程安全的双重锁

解决上面问题的方法便是在instance定义前加入volatile关键字。它将禁止在涉及instance的操作时,使用重排序。值得注意的是,重排序可能发生在编译在字节码阶段,jvm生成native code阶段,以及硬件执行阶段。

/**
 * @author eventime
 *
 * 单例模式与并发编程
 */
public class SInstance {
    private static volatile SInstance instance;
    public static SInstance getInstance() {
        if (instance == null) {
            int count = 1000000;
            while (count -- > 0);
            synchronized (SInstance.class) {
                if (instance == null) {
                    instance = new SInstance();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()-> System.out.println(SInstance.getInstance()));
        Thread t2 = new Thread(()-> System.out.println(SInstance.getInstance()));
        t1.start();
        t2.start();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值