单例模式之饿汉模式&懒汉模式

简介: 单例模式之饿汉模式&懒汉模式

a07cc3862c2748cfa6c92266f0164c51.png

前言

单例模式能保证某个类在程序中只存在唯一一份实例,而不会创建出多个实例,比如 JDBC 中的 DataSource 实例就只需要一个。单例模式具体的实现方式有"饿汉" 和 "懒汉" 两种。

1.饿汉模式(类加载的同时创建实例)

class Singleton {
    // 先创建出示例
    private static Singleton instance = new Singleton();
    // 如果需要使用这个唯一实例, 统一通过 Singleton.getInstance() 方式来获取.
    public static Singleton getInstance() {
        return instance;
    }
    // 把构造方法设为 private. 在类外面, 就无法通过 new 的方式来创建这个 Singleton 实例
    private Singleton() {}
}

此时饿汉模式即使在多线程情况下,也是线程安全的,因为只涉及到读操作。

2.懒汉模式

2.1单线程版的懒汉模式

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

如果此时这种懒汉模式放在多线程中是不安全的,因为既涉及到了读操作也涉及到了写操作。如果此时多个线程同时都读到了instance == null,那么就会多次进行new操作,这显然就不是单例了。

2.2进行加锁,对单线程版懒汉模式进行改进

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

这样进行加锁之后,就保证了读操作和修改操作是一个整体了。但是,目前进行加锁改进后的懒汉模式代码仍然还有问题,这样的加锁之后,就意味着每一次的getInstance都需要加锁,因为进行加锁操作是需要花费很大开销的,那么每一次都需要进行加锁,会浪费很大的开销。这里的加锁的本意是只在new出对象之前加,是有必要的,一旦new对象完成,后续加锁也就没有意义了,因为instance的值一定是非空的,直接就走return了。所以,还有改进的空间。

2.3继续改进(满足特定条件才进行加锁,也就是第一次new对象的时候才进行加锁)

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

第一层if (instance == null)判断是为了判断是否是第一次new对象,是否需要进行加锁;synchronized (SingletonLazy.class)此处就是在满足了特定条件后才进行加锁,即第一次new对象的时候;第二层 if (instance == null)是判断是否要创建对象。加锁操作可能会引起线程阻塞,当执行到锁结束之后,执行到第二个if 的时候第二个if和第一个if之间可能已经隔了很久的时间,程序的运行内部的状态,这些变量的值,都可能已经发生很大改变了,比如第一次都是null,有很多个线程都进入了第一个if,但是此时在synchronized这里阻塞等待,只有一个能进去,当后面的线程再次进去的时候,一定要靠第二个if来判断,因为此时instance很可能已经被第一次进去的线程给改了,所以很可能不为空,所以还需要第二个if来判断

但是此时,这段代码还有一些问题,那就是内存可见性问题。比如,同时有很多很多的线程都去getInstance,此时只有一个线程是真正进入了第二个if里面读取了内存,其他在外面的很多线程都是读寄存器/cache,那么此时编译器很可能会认为多次读取到的都是null,有可能会被优化处理。

还有就是在这个过程中,会涉及到指令重排序的问题。此处的instance = new SingletonLazy();可以拆分成三个步骤,(1)申请内存空间,(2)调用构造方法,把这个内存空间初始化成一个合理的对象,(3)把内存空间的地址赋值给instance引用。

在正常情况下,是按照123这个顺序来执行的,但是编译器为了提高程序效率,可能会调整代码的执行顺序,比如把123变成132。

假设t1是按照132的步骤来执行的,当t1执行到13步骤结束,2步骤之前的时候,被切出CPU了,换t2来执行。此时,t1执行完步骤3之后,在t2看来,此处的引用就非空了,此时此刻,t2就直接return了instance引用,并且可能会尝试使用引用中的属性。但是由于t1中的步骤2还没有完成,t2拿到的是非法的对象,还没有构造完成的不完整的对象。


2.4使用volatile关键字来解决内存可见性和禁止指令重排序的问题

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



相关文章
|
6天前
|
设计模式 安全 C#
单例模式详解
单例模式是一种常用的创建型设计模式,确保某个类只有一个实例,并提供一个全局访问点。本文详细介绍了单例模式的定义、特点、适用场景、优缺点及实现代码(C++ 和 C#),并探讨了线程安全的实现细节和与依赖注入的结合使用。
19 0
|
4月前
|
存储 设计模式 测试技术
单例模式
单例模式
|
6月前
|
设计模式 安全 Java
单例模式分享
单例模式分享
27 0
|
7月前
|
设计模式 安全
【单例模式】—— 每天一点小知识
【单例模式】—— 每天一点小知识
|
设计模式 安全 Java
单例模式的运用
单例模式的运用
46 0
|
设计模式 Java Spring
什么场景要使用单例模式,什么场景不能使用?
经常有小伙伴问我,设计模式学了这么久,每次看到概念也都能理解。但是,就是不知道怎么用,在哪里能用?我告诉大家,设计模式,不是为了要用而用的,而是作为前人总结下来的经验,等到哪天需要用的时候,你能想起来为你所用。
110 0
|
设计模式 C#
C# 机房重构单例模式
C# 机房重构单例模式
73 0
|
SQL 安全 Java
单例模式的理解
谈谈你对单例模式的理解。也算是老生常谈的问题了~~~
1059 1
|
设计模式 安全 Java
回顾一下单例模式
回顾一下单例模式
|
设计模式 缓存
我学会了,单例模式
单例模式属于创建型模式,这个类型的设计模式是将 对象的创建和使用解耦了,花式的去创建对象。
134 0
我学会了,单例模式