【单例深思】懒汉式改进版与内置锁

本文探讨了懒汉式单例模式的实现原理及线程安全问题,分析了为何原始懒汉式实现存在线程安全隐患,并介绍了如何通过synchronized关键字来解决这一问题。
我们知道 汉式的实现 延迟加载(Lazy Loading),但是不是 线程安全 的, 下面我们深入研究下为什么。

懒汉式的实现如下:

public   class  Singleton {
    private static Singleton singleton;
    private Singleton(){}
    public static Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

相比于饿汉式实现,懒汉式是 线程安全的,因为懒汉式中的类成员 singleton 在声明时没有立即使用 new   关键字实例化,而是在 getInstance() 方法里面才使用 new   进行实例化,此时 Singleton 的初始化不会实例化 singleton
只有当外部调用使用静态的 getInstance() 方法时, 类成员 singleton 才会被分配内存实例化,因此就达到了延迟加载的目的。
如有疑问,可参照 【单例深思】饿汉式与类加载  

接下来我们重点来看看懒汉式为什么不是线程安全的?

在多线程情况下,如果 singleton 还没有被实例化,此时它的值为null,如果这时有可能多个线程同时进入 getInstance() 方法中,同时执行   if  ( singleton  ==  null ) 这行代码,得到的结果都为true,于是这些线程都会使用 new  Singleton(); singleton 分配内存,这时 singleton   就不是单例了,所以 懒汉式 不是线程安全的。

懒汉式改进版解决了这个问题,其实现如下:

public   class  Singleton {
    private static Singleton instance;
    private Singleton(){}
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

其只改进了一个地方,就是使用关键字 synchronized   来声明 getInstance() 方法。这个关键字的作用就是给 getInstance() 加锁,加锁后这个方法就具备了原子性,每次只能由一个线程执行这个方法,其他请求线程则会被阻塞,直到活跃线程执行完毕。如果每次只有一个线程执行这个方法中的代码,那么我们上面讨论的线程安全问题就不复存在了,不会出现创建多个实例的情况了。

Java 提供了一种内置的锁机制来支持 原子性( 一组语句作为一个不可分割的单元被执行 同步代码块(Synchronized Block),同步代码块包括两部分,一个作为 对象引用,一个作为由这个锁保护的 代码块。以关键字 synchronized   来修饰的方法就是一种横跨整个方法体的同步代码块,其中该同步代码块的锁就是方法调用所在的对象。静态的 synchronized   方法以Class 对象作为锁。
synchronized(lock){
        //由锁保护的代码块
}  
每个Java对象都可以用做一个实现同步的锁,这些所被称为内置锁
线程进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁,而无论是通过正常的控制路径退出,还是通过从代码块中抛出异常退出。获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。

Java 的内置锁相当于一个互斥体(或互斥锁),这意味着最多只有一个线程能持有这种锁,当线程A尝试获取一个由线程B持有的锁时,线程A必须等待或阻塞,直到线程B释放这个锁。如果线程B永不释放这个锁,那么A就永远等待下去。

懒汉式改进版中 getInstance() 方法是静态的 synchronized   方法,因此以 Singleton.Class  作为锁, 线程只有获得了这个锁才能执行 getInstance() 方法,因此保证了线程安全性。但是这个锁只有一个,意味着同一时刻只能有一个线程执行该方法。如果这个单例很火,有很多线程需要获取它,那么就会影响单例的获取。这也是 synchronized   方法的一个弊端,代码的性能比较糟糕。这种简单且粗粒度的方法能确保线程安全性,但是不能同时处理多个请求,付出的代价很高。

这也就是双重检测锁实现出现的原因,通过缩小锁的粒度来增强活跃度,这将在下篇文章中详细讨论。













懒汉有不同的实现版本,下面从线程安全性、性能、实现复杂度等方面对常见的三个版本进行对比: ### 普通线程不安全版本 ```java class Singleton{ // 1.构造器私有化 private Singleton(){} // 2.静态实私有化 private static Singleton singleton; // 3.提供实的静态方法 public static Singleton getInstance(){ if (singleton == null){ singleton = new Singleton(); } return singleton; } } ``` - **线程安全性**:该版本不支持多线程,在多线程环境下不能正常工作。多个线程可能同时进入 `if (singleton == null)` 判断,从而创建多个实,违背了的原则 [^2][^4]。 - **性能**:由于没有加锁,在线程环境下性能较好,因为没有额外的同步开销。 - **实现复杂度**:实现简,代码量少,易于理解。 ### 同步方法版本 ```java public class LazySingleton { private LazySingleton() { } private volatile static LazySingleton instance; public synchronized static LazySingleton getInstance() { if (instance == null) { instance = new LazySingleton(); } return instance; } } ``` - **线程安全性**:使用 `synchronized` 关键字修饰 `getInstance` 方法,保证了在多线程环境下只有一个线程能进入该方法,从而确保的唯一性 [^1]。 - **性能**:由于每次调用 `getInstance` 方法都需要进行同步,会带来一定的性能开销,尤其是在高并发场景下,性能影响较为明显。 - **实现复杂度**:在普通版本的基础上增加了 `synchronized` 关键字,实现复杂度略有增加。 ### 双重检查锁定(Double-Checked Locking)版本 ```java public class LazySingleton { private LazySingleton() { } private volatile static LazySingleton instance; public static LazySingleton getInstance() { if (instance == null) { synchronized (LazySingleton.class) { if (instance == null) { instance = new LazySingleton(); } } } return instance; } } ``` - **线程安全性**:通过双重检查和 `synchronized` 块,既保证了在多线程环境下只有一个线程能创建实,又避免了每次调用 `getInstance` 方法都进行同步,解决了线程安全问题 [^5]。 - **性能**:只有在实未创建时才会进行同步,实创建后,后续调用 `getInstance` 方法无需同步,性能得到了优化。 - **实现复杂度**:代码相对复杂,需要理解双重检查和 `volatile` 关键字的作用。 ### 总结 | 版本 | 线程安全性 | 性能 | 实现复杂度 | | --- | --- | --- | --- | | 普通线程不安全版本 | 不安全 | 线程性能好 | 简 | | 同步方法版本 | 安全 | 有同步开销,性能一般 | 较简 | | 双重检查锁定版本 | 安全 | 性能优化 | 复杂 |
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值