设计模式之单例模式

本文详细介绍了单例模式的概念及其在Java中的实现,包括饿汉式和懒汉式。针对多线程环境下懒汉式的线程安全问题,文章通过实例展示了可能出现的问题,并逐步优化解决方案,最终引入volatile关键字防止指令重排,确保线程安全。
摘要由CSDN通过智能技术生成

前言

以下只是我个人在学习设计模式的过程中的笔记,并不专业,各位大佬如果发现错误的话,欢迎在评论区指出。

单例模式介绍

在开发过程中,很多对象其实我们只需要一个(线程池,缓存…),而单例模式确保一个类只有一个实例,并提供一个全局访问点,如果想要访问此实例就必须通过全局访问点。

单例模式的实现过程

单例模式分为两种方式,饿汉式和懒汉式,下面将分别介绍

1、饿汉式

先上代码

public class Singleton {
    private static Singleton singleton = new Singleton();
    private Singleton() {
    	System.out.println("执行了构造函数");
    }
    public static Singleton getInstance(){
        return singleton;
    }
}

饿汉式要求在JVM加载此类时马上创建此类的唯一单例。这样就保证了此后无论谁来获取此对象拿到的都是唯一单例。饿汉式虽然简单,但是它在初始化时便创建好了,有可能我们在整个项目中都没有用到这个实例,这个时候就会产生浪费,因此这种方式一般会在项目应用的运行压力不大时使用。

2、懒汉式

先来看最简单的一种懒汉式

public class Singleton {
    private static Singleton singleton;

    private Singleton() {
    	System.out.println("执行了构造函数");
    }

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

由上面的代码可以看出懒汉式的核心,便是延迟实例化,将实例的创建推迟到第一次使用时,当代码的某个地方需要使用singleton单例,便调用getInstance方法,这时便会判断该单例是否已被实例化,若已实例化,便直接返回该实例,若没有则进行实例化后再返回。

上面这段代码看似没什么问题,但是却只适用于单线程的情况,我们开辟十个线程同时获取单例看看:

public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> System.out.println(Singleton.getInstance())).start();
        }
    }

获取结果
可以看到构造函数执行了三次,这代表出现三个不同的单例,打印出来的对象也证明了是三个不同的。为什么会产生这种情况,是因为在多线程的情况下,可能会有多个线程通过了singleton==null的判断,这样的话便会有多个线程执行 singleton = new Singleton(),这样便会产生多个不同的对象。

那么我们如何来解决该问题呢?答案是使用synchronized,关键字 synchronized可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块,改进后的代码如下:

public class Singleton {
    private static Singleton singleton;

    private Singleton() {
        System.out.println("执行了构造函数");
    }

    public static Singleton getInstance(){
        synchronized(Singleton.class){
        	System.out.println(Thread.currentThread().getName()+"进入了同步区");
            if (singleton == null){
                singleton = new Singleton();
            }
        }
        return singleton;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> System.out.println(Singleton.getInstance())).start();
        }
    }
}

此时我们在尝试去使用多线程获取单例
![在这里插入图片描述](https://img-blog.csdnimg.cn/c926862667384b6ba6f04d881c2c4565.png?x-oss-process=image/watermark,type_ZmFu

可以看到在10个线程获取单例的情况下构造函数只执行了一次。打印出来的对象也只有一个。问题看似解决了,皆大欢喜。但是!我们会发现在上面的代码中也有一个问题,那就是每个代码都进入了同步区,在高并发的情况下,大量线程去竞争一把锁,这会导致效率的降低。所以我们可以对上面的代码进行优化。在进入同步区之前进行一次判断,降低进入同步区的线程的数量。代码如下:

public class Singleton {
    private static Singleton singleton;

    private Singleton() {
        System.out.println("执行了构造函数");
    }

    public static Singleton getInstance(){
        if (singleton==null){
            synchronized(Singleton.class){
                System.out.println(Thread.currentThread().getName()+"进入了同步区");
                if (singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> Singleton.getInstance()).start();
        }
    }
}

这次我们把打印的无关信息去掉,着重观察进入同步区的线程与之前有什么变化
在这里插入图片描述
这次可以看到进入同步区的线程数量明显减少。这回总该没有问题了吧,当然不是>_<。在极端情况下是会出现问题的。
在这里插入图片描述
这里就要说到实例化对象的过程了,new一个对象的过程其实分成了好几步:

在这里插入图片描述
而jvm在执行这些指令的时候并不是按照1 2 3 这样的顺序执行的,也可能是按照1 3 2 这样的顺序执行,这个时候问题就来了,假如说线程A通过了两次singleton的判断进入同步区开始实例化对象,线程A按照1 3 2的顺序刚执行完1和3步骤,这时候一个线程B也想来获取单例,它在第一层判断时,发现singleton不为null,这时候线程B就会认为singleton已经被实例化完成了可以直接使用,于是直接返回了singleton
的地址,但是这时singleton的内存里面还是一片虚无,这时候直接使用的话就会出现问题。

那如何解决它呢?使用volatile,volatile关键字可以避免指令重排,什么意思呢?就是让实例化对象的过程按前面说的1 2 3顺序来执行。这样就可以避免对象还未实例化完成就直接使用。最后的代码便是这样:

public class Singleton {
    private volatile static Singleton singleton;

    private Singleton() {
        System.out.println("执行了构造函数");
    }

    public static Singleton getInstance(){
        if (singleton==null){
            synchronized(Singleton.class){
                System.out.println(Thread.currentThread().getName()+"进入了同步区");
                if (singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> Singleton.getInstance()).start();
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值