认识单例模式

本文介绍了单例模式的概念、优点,以及两种常见的实现方式——饿汉模式和懒汉模式。在多线程环境下,懒汉模式可能引发线程安全问题,为此,文章详细讨论了解决这一问题的双重检查加锁(DCL)策略,并解释了volatile关键字在确保内存可见性中的作用,以实现线程安全的单例模式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、什么是单例模式

   单例模式是设计模式的一种,为了保证整个系统中一个类只有一个对象的实例。就比如我们之前在JDBC编程中使用DataSource,我们就希望只需要与数据库建立一次连接,就可以一直使用,而不用反复创建,这时我们就可以使用单例模式。单例模式主要有以下两个优点

  1. 节省公共资源,我们在创建一个类的实例时,可能是需要很多计算机资源的,如果我们反复创建的话,就会浪费许多资源。
  2. 方便于管理和控制,一个类的实例中可能有很多个变量,如果同时有很多个程序在修改,可能会造成数据不准确(线程安全问题),如果想要保证数据的准确性,就需要对关键代码上锁,而上锁就会导致效率降低,所以我们就规定只有一个程序能修改,就是单例模式。

二、实现单例模式

  1. 构造方法私有:我们希望这个类不能通过new关键字来创建一个实例,所以我们第一步就需要让构造方法私有
  2. 设置静态方法:我们希望这个类对外提供一个方法,这个方法可以直接由类名调用,然后返回一个类的实例,所以我们构造一个静态方法
  3. 设置静态成员变量:我们希望每次调用这个方法都能获得同一个类的实例,所以我们就要设置一个静态成员变量

具体有两种单例模式

1、饿汉模式

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

  饿汉模式虽然可以达到单例的目的,但是却还是造成了资源上的浪费,把这个类的实例创建出来之后并没有用,占用了内存空间,所以我们在饿汉模式的基础上又做了进一步的调整,也就是懒汉模式。

2、懒汉模式

public class Singleton {
    public static Singleton instance = null;

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

    private Singleton(){}
}

  在实际开发中,懒汉模式更常用一些,因为他更高效也更节省资源,但是也不绝对,在某些特定场合下,饿汉模式就可能比懒汉模式更好用。

懒汉模式在多线程情况下的问题

  懒汉模式解决了饿汉模式带来的资源浪费问题,只在调用方法时才创建对象。但是在多线程情况下就可能发生线程安全问题,创建多个实例。以下面这个例子来说,线程2在从内存中读取数据时,线程1 还没有把创建好的实例储存起来,所以线程2获取到的还是一个空的实例,所以就会再创建一个新的实例。
在这里插入图片描述
  针对上述问题其实就是线程安全问题,我们能想到的办法就是加锁。因为这个代码只有在if条件判断时才设计到了一次LOAD操作,所以我们就需要把锁加在if之前。

public class Singleton {
    private static Singleton instance = null;

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

    private Singleton(){}
}

那么这样是不是就完全没问题了呢?对安全性来说,是这样的,这样的单例模式可以保证线程的安全,但是因为锁是一个比较重量的操作,当给这段代码加上了锁之后,效率就会下降很多,所以一些大佬们就在想有什么方法可以既保证安全性还能保证效率,分析问题就得出,其实单例模式只有在初始化instance时才会出现线程安全问题,一旦instance已经初始化完毕了,就不涉及线程安全问题了,所以就引入了双重检查加锁(DCL)。

public class Singleton {
    private static Singleton instance = null;

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

    private Singleton(){}
}

  用这种方法就可以只在前几次初始化的时候才调用到锁来保护线程的安全,后续的调用都不会进入第一个if也就不会触发锁,效率自然就高了。
在这里插入图片描述
这样是不是就是一个最合理的单例模式了呢?我们来看下一个问题。如果按照人的思维来说,这就是一个完美的单例模式了,但是从计算机的角度来看就不完美了,因为在多线程环境下,就涉及到了内存可见性问题,计算机就会以它认为合理方式对代码中的操作做出了优化,也就是读取intance时直接从寄存器中读,不进入内存,这样的话前面线程修改过的intance就不能及时反映给下面的线程,也就会导致创建多个实例。所以我们需要用volatile关键字来修饰intance变量,来保证内存可见性,每次读取的值都是内存中最新的值。

最终代码

public class Singleton {
    volatile private static Singleton instance = null;

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

    private Singleton(){}
}

总结

保证线程安全的单例模式主要涉及以下三个点:

  1. 加锁:在合适的位置加锁,要保证把if和new过程包裹起来,还要尽量保证包裹最少的代码,以保证代码的效率。
  2. 双重if判断:保证效率的最核心方法,在初始化时才用加锁。
  3. volatile关键字:保证外层if读操作读到的都是内存中最新的值。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值