zl程序教程

您现在的位置是:首页 >  其他

当前栏目

单例设计模式

设计模式 单例
2023-09-27 14:25:52 时间

在这里插入图片描述


前言

在有些系统中,为了节省内存资源、保证数据内容的一致性,对某些类要求只能创建一个实例,这就是所谓的单例模式。

一、单例模式

单类模式能够保证某个类在程序中只存在一个实例,而不会创建多个实例:比如我们JDBC的DataSource实例,Windows的回收站,多线程的线程池等等

单例模式有 3 个特点:

  1. 单例类只有一个实例对象;
  2. 该单例对象必须由单例类自行创建;
  3. 单例类对外提供一个访问该单例的全局访问点;

单例模式有很多种,我们重点介绍饿汉模式和懒汉模式

饿汉模式

class Singleton{
    //直接创建一个实例
    private static Singleton instance = new Singleton();

    //私有的构造方法
    private Singleton(){

    }
    //通过Singleton.getInstance获取该实例
    public static Singleton getInstance() {
        return instance;
    }
}

在这里插入图片描述
我们这个属性是类属性,与实例无关,我们java的每个类,在编译完成后都会得到一个.class文件,JVM运行时就会加载这个.class文件,读取其二进制指令,并在内存中构造出对应的类对象(Singleton.class)
在这里插入图片描述
当我们想手动的去创建一个Singleton对象时,发现报错了,Singleton()方法是私有的。
在这里插入图片描述
我们可以发现,我们在设计单例模式的时候,把构造方法设置为私有的了,就是防止外面的代码去new 对象。
在这里插入图片描述
我们对外提供获取的实例的方法,设置为public static,该方法为类方法,通过类名即可调用。
为什么这种方法叫做饿汉模式?
在这里插入图片描述
因为我们在类加载阶段就将实例创建出来了,类加载是我们程序比较靠前的一个阶段,所以我们形象的把它称之为饿汉模式。

懒汉模式

class Singleton{
    private static Singleton instance = null;
    private Singleton() {

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

我们有了饿汉模式的基础,我们对懒汉模式更好理解了。
在这里插入图片描述
我们可以发现,我们懒汉模式并没有在类加载阶段就创建实例,因为它很懒
在这里插入图片描述
我们可以发现,懒汉模式,只有在我们第一次调用时才会去创建对象,如果不用就不会创建对象。

二、线程安全问题

我们刚才所介绍的两种单例模式,在单线程下是安全的,我们来分析一下在多线程条件下,是否安全?
在这里插入图片描述
我们可以发现,我们的饿汉模式只涉及到读操作,在我们前面学习的线程安全问题可以知道,多个线程读一个数据,是线程安全的。
在这里插入图片描述

我们可以看到我们的懒汉模式,既涉及到了读操作,有涉及到了写操作。
在这里插入图片描述
我们发现多线程下的懒汉模式,可能会触发多少new操作,与单例模式的理念相反,所以我们的懒汉单例模式是线程不安全的。
既然懒汉模式涉及到读写问题,那我们就加锁呗。

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

这样加锁可以吗?
不可以,刚才的线程问题,实际上就是读比较写三个操作不是原子的,给写加锁,仍然会造成脏读问题。

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

我们把锁加在外面,才能保证读,比较,写三个操作为原子的。
在这里插入图片描述

我们保证原子性之后,我们t2在想去读的时候,就会阻塞等待,知道t1修改保存之后,t2才能load,此时load的结果是t1修改之后的值了,也就不会new对象了。
虽然保证了单例,但是我们的代码还是有一些不足之处。
在这里插入图片描述
我们多线程来获取实例时,不管该实例是否被创建,都会加锁,而加锁的开销又是比较大的,我们这个真的需要每次获取实例都加锁吗?

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

此时我们在加锁之前加一条判断,判断实例是否被创建,如果创建了就不再加锁,如果没有创建在进行加锁操作。
但是即使我们加了这样的判断,就一定能保证不会在实例创建后加锁吗?
在这里插入图片描述
当我们有很多线程,都在获取实例,我们CPU可能会进行优化,只有第一次读的是主内存的,修改后,后面的线程都是读的未更新的工作内存的值,这样仍会多次加锁,造成不必要的开销。

class Singleton{
    private volatile static Singleton instance = null;
    private Singleton() {

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

在这里插入图片描述
我们为了安全起见,我们给实例属性加上volatile关键字修饰,彻底去避免内存可见性问题和指令重排序问题。