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

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

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



相关文章
|
11月前
|
设计模式 存储
static应用之 单例设计模式(饿汉单例&懒汉单例)
本章我们来学习单例模式中的饿汉单例和懒汉单例,那么什么是单例模式呢?应用该模式的这个类永远只有一个实列,即一个类只能创建一个对象例如电脑上的任务管理器对象只需要一个就能解决问题,可以节省内存空间先定义一个类,把构造器私有如下图,先来看一下没有把构造器私有化的SingleInstance类,此时Test类中可以随意创建多个SingleInstance的实例化。 在SingleInstance类中用private修饰无参构造器,此时左边new方法报错了。我们在右边创建一个静态变量来存储对象,变量名为instan
47 0
|
5月前
|
设计模式 安全 Java
单例模式:饿汉模式、懒汉模式
单例模式:饿汉模式、懒汉模式
75 0
|
4月前
|
安全 Java
线程安全的单例模式(Singleton)
线程安全的单例模式(Singleton)
|
5月前
|
SQL 安全 Java
懒汉式单例的3个坑
懒汉式单例的3个坑
|
安全
线程安全的单例懒汉式
线程安全的单例懒汉式
39 0
|
安全 Java
懒汉式单例
懒汉式单例
|
缓存 安全 Java
双重检查锁单例
双重检查锁单例
|
安全 Java
饿汉式单例
饿汉式单例
单例模式(懒汉和饿汉)——独生子女挺好
单例模式(懒汉和饿汉)——独生子女挺好
|
设计模式 安全 Java
Java多线程案例-单例模式【饿汉模式,懒汉模式】
Java多线程案例-单例模式【饿汉模式,懒汉模式】
Java多线程案例-单例模式【饿汉模式,懒汉模式】