单例学习篇:


懒汉式单例模式 在没有进行适当的同步时,确实是线程不安全的。

什么是懒汉式单例?
懒汉式单例是指实例的创建被推迟到 第一次 使用时(即 懒加载)。相比于在类加载时就初始化实例的饿汉式单例,懒汉式的优点是只有在真正需要时才会创建实例,节省资源。

线程不安全问题

好处:



new Thread为什么不能直接跑线程

public class Singleton {
    // 使用 volatile 确保 instance 的可见性和防止指令重排序
    private static volatile Singleton instance;

    // 私有构造函数,防止外部实例化
    private Singleton() {}

    // 获取实例的双重检查锁定方法
    public static Singleton getInstance() {
        // 第一次检查(未加锁)
        if (instance == null) { 
            synchronized (Singleton.class) {  // 同步锁
                // 第二次检查(加锁后):这里加了判断:看下面解释1
                if (instance == null) {  
                    instance = new Singleton();  // 创建实例
                }
            }
        }
        return instance;
    }
}



//  第二次检查(加锁后):这里加了判断:看下面解释1:

但是你提到的“为什么还能同时两个线程进去”的问题,其实并不是两个线程能同时进入同步块,而是双重检查锁定中存在两个检查步骤,所以看起来像是两个线程“同时”进入了创建实例的逻辑。实际上,这两个检查的时间节点是不同的。

让我们详细分析一下这个过程,澄清几个关键点:

1. 第一次检查(非同步的部分)
在双重检查锁定的实现中,第一次检查是在进入 synchronized 块之前进行的:

1、
if (instance == null) {  // 第一次检查
    synchronized (Singleton.class) {
        if (instance == null) {  // 第二次检查
            instance = new Singleton();
        }
    }
}

在这个步骤,多个线程可以同时执行第一次检查,因为此时还没有加锁。假设两个线程 A 和 B 同时执行了 getInstance() 方法,发现 instance == null,于是它们都试图进入同步块。
2. 第二次检查(同步块内的检查)
虽然两个线程 A 和 B 都通过了第一次检查,但它们会竞争锁,只有一个线程能首先进入同步块,另外的线程会在 synchronized 块外等待。

假设线程 A 获得了锁,进入了同步块,它会进行第二次检查。
线程 B 在同步块外等待锁被释放。
3. 锁的行为:互斥性
在 synchronized 块内部,互斥行为确保了一次只有一个线程可以执行同步块的代码。比如线程 A 获得锁后,线程 B 必须等待,不能同时执行同步代码。

4. 为什么需要第二次检查
当线程 A 进入同步块并成功创建了实例 instance 后,它会释放锁。这时线程 B 获取锁,并进入同步块,但因为线程 A 已经完成了实例创建,线程 B 再次检查 instance == null,发现实例已经被创建,因此不会再创建新的实例。

这就是为什么即使线程 B 也通过了第一次检查(if (instance == null)),在同步块内还需要进行第二次检查的原因。如果没有第二次检查,线程 B 进入同步块后会重新创建一个新的实例,破坏了单例模式的唯一性。

总结:synchronized 确保互斥,双重检查确保正确性
synchronized 确保了一次只有一个线程可以进入同步块,因此不会有两个线程同时进入。
双重检查的设计确保了在并发环境中,只有一个线程能创建实例。如果没有第二次检查,即使线程 B 等待锁释放后进入同步块,它也可能会再次创建实例,破坏单例的唯一性。
因此,synchronized 确实是一个锁,并能保证互斥访问。多个线程不能同时执行同步代码,但在 synchronized 之外的代码可以并发执行,所以需要双重检查来确保线程安全。






volatile:
作用: 变量的可见性 和 有序性,特别是在多线程编程中。volatile 通常用于那些多个线程会共享、读取、修改的变量。它告诉 JVM 不要对这些变量进行线程的本地缓存优化,而是每次都从主内存中读取,并且在变量被修改时立即将其写回主内存
volatile 的语法:
private volatile int sharedVariable;



当一个变量被声明为 volatile 时,它具备以下两个特性:

可见性:当一个线程修改了 volatile 变量的值时,其他线程能够立即看到这个改变,而不是依赖于各自的线程缓存。
禁止指令重排序:对 volatile 变量的读写操作不会与其他读写操作发生重排序。


volatile 的使用场景:
标志位变量:通常用于控制线程的运行状态,如终止线程的标志位。
单例模式:使用 volatile 来防止指令重排序,从而保证双重检查锁定(Double-Checked Locking)中的线程安全性。

饿汉式加载:

public class Singleton {
    // 静态变量,类加载时就创建
    private static final Singleton instance = new Singleton();

    // 私有构造函数,防止外部实例化
    private Singleton() {}

    // 提供获取实例的全局访问点
    public static Singleton getInstance() {
        return instance;
    }
}

总结:

饿汉式与懒汉式的对比

特点饿汉式懒汉式
实例创建时机类加载时立即创建第一次使用时延迟创建
线程安全性天然线程安全需要额外的同步措施以确保线程安全
实现难度简单需要双重检查锁定、同步等机制确保线程安全
资源利用效率若不使用实例则会浪费资源延迟加载,资源利用效率较高
适用场景实例比较轻量、并且程序启动后可能立即用到实例创建耗时较长,且并不一定会用到的场景

适用场景

  • 实例轻量:如果单例对象初始化非常快,占用资源较少,可以使用饿汉式。
  • 程序启动后立即需要使用实例:比如一些配置类、全局管理类,如果在程序启动时就需要用到,可以用饿汉式保证初始化的及时性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值