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

7,789次阅读
没有评论

共计 3060 个字符,预计需要花费 8 分钟才能阅读完成。

经典单例模式

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


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。


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。


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 阶段,以及硬件执行阶段。


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();
    }
}

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

    正文完
     0
    Yojack
    版权声明:本篇文章由 Yojack 于2024-11-10发表,共计3060字。
    转载说明:
    1 本网站名称:优杰开发笔记
    2 本站永久网址:https://yojack.cn
    3 本网站的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,请联系站长进行删除处理。
    4 本站一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
    5 本站所有内容均可转载及分享, 但请注明出处
    6 我们始终尊重原创作者的版权,所有文章在发布时,均尽可能注明出处与作者。
    7 站长邮箱:laylwenl@gmail.com
    评论(没有评论)