Java 线程知识笔记 (八) 单例模式的演变

本文深入探讨了Java中的单例模式,包括饿汉模式、懒汉模式、懒汉模式+同步锁(DCL)、Holder模式和Enum模式。详细分析了各种模式的线程安全、延时加载、空间占用和性能特点,为读者提供了实现单例模式的多种策略和最佳实践。

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

前言

很多同学都在学习Java的过程中听说过单例模式,或者在面试中遇到过类似的问题。单例模式算是老生常谈的问题了,其实就是一句话能说清楚事情:所谓单例模式,就是实例化过程中只实例化一次。无论有多少线程来访问,都只实例化一次,多个线程调用已经实例化好的对象,而不是重新创建一个。虽说简单但是还是有不少细节要深究一些,比如前面有些文章里说的DCL就是单例模式的实例之一。更多线程知识内容请点击【Java 多线程和锁知识笔记系列】

为什么要有单例模式

既然要说单例模式还是从零开始,设想这样一个场景:远程办公,有一个任务文档要写,很多同事必然要打开这个文档去输入内容,如果给每一个同事分配一个文档实例,那就相当于谁写谁的,想要知道大家都写了什么必须整合在一起,费时费力。因此所有人如果能够使用同一个文档实例,大家都把自己的内容直接整合进去,就能省去很多时间,当然我们这里不考虑文档顺序或者复盖这些问题,只考虑实例对象。这个例子抽象出来就是:多个线程操作同一个对象的时候,要保证对象的唯一性,这个唯一性就是单例模式。

单例模式的通解

一般说道模式,就和模板类似,不仅Java可以实现,任何一种编程语言只要套用这个模板都可以实现这个模式。一般来说这个通解就是:首先要保证有且只有一个实例化的过程,也就说产生实例化对象(new)的过程只能有一次;然后提供一个返回实例化对象的方法供外部使用,常见的getInstance()等等。凡是遵循上述通解模式的代码开发,都叫做单例模式。下面就介绍一些常见的单例模式。


饿汉模式

这种实现模式是最基础的单例模式的实现。所谓的饿汉的意思:就是说我用不用实例对象不管,反正立刻得有。

public class HungryModel {
    /**
     * 这就是一个标准的饿汉模式,因为static标注的成员变量,
     * 会在最开始被加载的时候就产生实例对象。
     * 用不用不管,反正得现有吃的再说,这就是饿汉
     */
    private static HungryModel instance=new HungryModel();
    private HungryModel() { } //构造方法私有,不允许外部使用new创造一个对象出来
    //返回实例对象的方法
    public static HungryModel getInstance(){
        return instance;
    }
    //用对象的hashcode进行测试
    public static void main(String[] args) {
        for (int i = 0; i <5 ; i++) {
            new Thread(()->{
                System.out.println(HungryModel.getInstance().hashCode());
            }).start();
        }
    }
}
打印输出,所有线程使用对象的hashcode相同,说明没有新的对象被创建出来,单例模式成功
2040633862
2040633862
2040633862
2040633862
2040633862

实例出来了,我们分析下饿汉模式的优缺点:

  • 线程方面,例子中static变量在类加载(ClassLoader)的时候就会被实例化,因此只会有这一次,不会出现多次实例化的过程,所以是线程安全的。
  • 延时加载方面,不会延迟加载,无论用不用都会被加载出来。
  • 空间方面,由于会自动加载,无论使用不使用都要占据内存的空间,如果实例对象很大,又很多有概率会导致内存溢出。
  • 性能方面,如果实例对象比较小,性能会比较好。

懒汉模式

懒汉模式是对饿汉模式的一种改进。刚才已经分析过,饿汉模式不会在意实例对象是不是使用都会创建出来。相对的懒汉模式,就是当需要使用的时候再进行实例化。不用就不动,所谓的懒也就是从这里来的。

public class LazyModel {
    private static LazyModel instance;
    private LazyModel() { } //构造方法私有,不允许外部使用new创造一个对象出来
    //实例化部分与返回部分合在一起,是懒汉模式的标志特点
    public static LazyModel getInstance(){
        if (Objects.isNull(instance)){
            instance=new LazyModel();
        }
        return instance;
    }

    public static void main(String[] args) {
        for (int i = 0; i <5 ; i++) {
            new Thread(()->{
                System.out.println(LazyModel.getInstance().hashCode());
            }).start();
        }
    }
}
打印输出,所有线程使用对象的hashcode相同,说明没有新的对象被创建出来,单例模式成功
1934637030
1934637030
1934637030
1934637030
1934637030

在这个例子中,当调用到getInstance()的时候才会把相应的实例对象给创建出来,所谓的懒也就是指这个步骤。继续分析下懒汉模式的优缺点:

  • 线程方面,由于实例是调用时创建,如果有两个线程同时执行到Objects.isNull(instance)这个条件判断的时候,还是有机会创建两个实例对象的,因此线程不安全。多线程下并不能保证实例对象的唯一性,风险很大。
  • 延时加载方面,延迟加载,什么时候用什么时候创建。
  • 空间方面,延时加载,不调用不占内存空间。
  • 性能方面,什么时候用,什么时候加载,加载过以后就不再创建新的实例对象,性能高。
    由于多线程有不安全的问题,一票否决,多线程下不要使用这种模式。

懒汉模式 + 同步锁

懒汉模式有线程安全的缺点,可以通过加一个synchronized同步锁解决这个问题。

public class LazySyncModel {
    private static LazySyncModel instance;
    private LazySyncModel() { } //构造方法私有,不允许外部使用new创造一个对象出来
    //实例化部分与返回部分合在一起,是懒汉模式的标志特点
    public synchronized static LazySyncModel getInstance(){
        if (Objects.isNull(instance)){
            instance=new LazySyncModel();
        }
        return instance;
    }
}

基本上代码是一样的,优缺点基本上也是一样的,唯一就是多线程上做了控制:

  • 线程方面,由于在调用创建实例的时候加了同步锁,因此当有一个线程访问的时候,其他线程想要访问时会blocked无法访问,因此线程安全。
  • 性能方面,由于创建时加了synchronized,因此每到需要使用实例变量的时候,就要排队,执行过成由多线程并行执行变成了串行执行,性能下降。
  • 其他方面,同懒汉模式。

Double Checked Locking (DCL)

双重检查锁定模式。懒汉模式+同步锁会导致线程排队,但是我们真正想要做的只是想要创建的时候不能多线程执行而已,因此我们可以直接把锁加在new这个语句上,做下面的修改:

if (Objects.isNull(instance)){ 
    synchronized(LazySyncModel.class){
        instance=new LazySyncModel();
    }
}

但是这样做还是有问题,和原始的懒汉模式一样:假设有两个线程同时执行到Objects.isNull(instance)这个条件判断的时候,都判断条件为true要进入语句块内。此时线程1拿到了资源去创建了,于是线程2等待线程1释放资源。等线程1运行结束以后,线程2还是要往下走,直接又创建一次。所以这样线程就又不安全了,怎么样才能保证线程安全呢,我们继续修改代码:

public class DCLModel {
    private static DCLModel instance;
    private DCLModel() { } //构造方法私有,不允许外部使用new创造一个对象出来
    
    public static DCLModel getInstance(){
        if (Objects.isNull(instance)){
            synchronized(DCLModel.class){
                //再次检查是不是真的要创建实例对象
                if(Objects.isNull(instance)){
                    instance=new DCLModel();
                }
            }
        }
        return instance;
    }
}

这次的修改是在同步锁synchronized块里面再次判断实例对象要不要创建,这种同步锁外面用if语句检查一次,里面再检查一次的方法,就叫做Double Checked Locking (DCL)。所以Double Check双重检测,就是这么来的。接下来我们分析下这种模式的优缺点:

  • 线程方面,没问题线程安全,这是一定的。
  • 延时加载方面,是延时加载。
  • 空间方面,随调用生成,不调用不占空间,也不错。
  • 性能方面,比懒汉-同步锁模式有优化,比纯懒汉模式安全。

似乎看起来很完美,但是还要考虑到另一个问题,在说volatile关键字的时候,会有指令重排的问题,那么极小概率情况下,会导致其他线程在使用实例对象的时候报空指针异常。比如,线程1实例化对象结束但是还没有给其中的成员对象赋值完成的时候,线程2就开始使用这些成员变量,就会引起空指针异常,因此最好我们在实例对象声明的地方加上volatile多做一个保险:

public class DCLModel {
	//用volatile保证代码执行顺序
    private volatile static DCLModel instance; 
    private DCLModel() { } //构造方法私有,不允许外部使用new创造一个对象出来
    public static DCLModel getInstance(){
        if (Objects.isNull(instance)){
            synchronized(DCLModel.class){
                if(Objects.isNull(instance)){
                    instance=new DCLModel();
                }
            }
        }
        return instance;
    }
}

此外即使为了阻止别人New一个单例类,把构造方法全部设置为私有的,其实这也只是 “防君子不妨小人” 的措施而已,毕竟Java提供了反射技术可以对这一个过程进行逆向,但是作为开发者只能保证自己的写法相对安全而已。


Holder 模式

上面的例子中所有的实例对象都是作为成员变量存在的,Holder模式则是放到一个内部静态类中,然后通过调用静态内部类的实例对象,提供对外的访问机制。

public class HolderModel {
	private HolderModel() { } //构造方法私有,不允许外部使用new创造一个对象出来
    //主动调用,才会实例化
    public static HolderModel getInstance(){
        return InstanceHolder.instance;
    }
    //使用静态内部类进行实例化
    private static class InstanceHolder{
        private static HolderModel instance=new HolderModel();
    }
}

由于使用静态类,那么在实例化的时候只能实例化一次,因此线程的安全性得到了保证。而且很明显,这样做避免了加锁,因此性能上又有所提高。由于内部类只有主动调用的时候才会实例化,因此也能做到随用随加载。可以说Holder模式结合了懒汉模式和饿汉模式,属于十分高效安全的单例模式。


Enum模式

最后一种是枚举模式,是Effective Java书中中提到方式,主要使用的就是枚举类型的特性。这种特性就是枚举类型都是属于常量,而且只能在加载的时候实例化一次,就是说不能被懒加载。

public enum EnumModel {
    INSTANCE;
    public static EnumModel getInstance(){
        return INSTANCE;
    }
}

代码非常的简洁,但是却能够实现单例模式还可以保证线程的安全性,围观Java大神的代码,膜拜一下。由于这种模式无法直接实现懒加载,那么稍微改进一下,把这种Enum模式Holder模式结合起来就能二次提高性能,所谓站在巨人的肩膀上看的更远。

public class EnumModel {
    private EnumModel(){} //构造方法私有,不允许外部使用new创造一个对象出来
    private enum EnumHolder{
        INSTANCE;
        private EnumModel instance=null;
        EnumHolder(){
            this.instance=new EnumModel();
        }
    }
    //延时加载,当需要的时候调用内部枚举类型创建实例
    public static EnumModel getInstance(){
        return EnumHolder.INSTANCE.instance;
    }

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

在改进过的代码里面,我们将静态内部类替换为枚举类,在枚举类调用的时候进行外部类的实例化,并且通过最终在外部调用枚举类型完成单例模式的实现。到此单例模式基本结束。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值