单例设计模式

本文详细介绍了单例设计模式的原理、使用场景以及五种常见实现方式,包括饿汉式、懒汉式、双重检查锁、静态内部类和枚举。同时讨论了单例模式的局限性,如缺乏继承和多态,以及在实际项目中的扩展挑战。
摘要由CSDN通过智能技术生成
简介:

单例设计模式(Singleton Design Pattern)理解起来非常简单。一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。

一、为什么要使用单例
1、表示全局唯一

如果有些数据在系统中应该且只能保存一份,那就应该设计为单例类。如:

  • 配置类:在系统中,我们只有一个配置文件,当配置文件被加载到内存之后,应该被映射为一个唯一的【配置实例】,此时就可以使用单例,当然也可以不用。
  • 全局计数器:我们使用一个全局的计数器进行数据统计、生成全局递增ID等功能。若计数器不唯一,很有可能产生统计无效,ID重复等。
public class GlobalCounter {
    private AtomicLong atomicLong = new AtomicLong(0);
    private static final GlobalCounter instance = new GlobalCounter();
    // 私有化无参构造器
    private GlobalCounter() {}
    public static GlobalCounter getInstance() {
        return instance;
    }
    public long getId() { 
        return atomicLong.incrementAndGet();
    }
}
// 查看当前的统计数量
long courrentNumber = GlobalCounter.getInstance().getId();

以上代码也可以实现全局ID生成器的代码。

2、处理资源访问冲突

处理资源访问冲突问题,有些情况可以对方法加锁,但是要注意,加锁后(如果方法中没有加static,锁的是this(方法的调用者),如果加了static,锁的就是class),Tomcat处理每一个Controller都会开启一个独立的线程去执行。

Java里面就有一个天然的单例对象,就是class对象,每一个类在加载到内存的时候,只会生成一个class对象,而静态方法和静态变量是属于class的

二、如何实现一个单例?

常见的单例设计模式,有如下五种写法,在编写单例代码的时候要注意以下几点:

1、构造器需要私有化

2、暴露一个公共的获取单例对象的接口

3、是否支持懒加载(延迟加载)

4、是否线程安全

1、饿汉式

饿汉式的实现方式比较简单。在类加载的时候,instance 静态实例就已经创建并初始化好了,所以,instance 实例的创建过程是线程安全的。从名字中我们也可以看出这一点。具体的代码实现如下所示:

/**
 * 饿汉式单例的实现
 */
public class EagerSingleton {

    //1.持有一个jvm全局唯一的实例
    private static final EagerSingleton instance = new EagerSingleton();

    //2.私有化构造器
    private EagerSingleton(){}

    //3.对外提供一个获取实例的方法
    public static EagerSingleton getInstance(){
        return instance;
    }
}

2、懒汉式

特点都写在代码的注释里,这里不再赘述。


/**
 * 懒汉式单例的实现
 */
public class LazySingleton {

    //1.持有一个jvm全局唯一的实例
    private static LazySingleton instance;

    //2.私有化构造器
    private LazySingleton(){}

    //3.对外提供一个获取实例的方法
    public synchronized static LazySingleton getInstance(){
        //这里如果不加上synchronized,会有线程安全问题
        //但是加上锁会影响性能
        if(instance == null){
            //只有在用到的时候,才去创建对象,但对象在创建的过程中可能极其复杂
            instance = new LazySingleton();
        }
        return instance;
    }
}
3、双重检查锁

饿汉式不支持延迟加载,懒汉式有性能问题,不支持高并发。那我们再来看一种既支持延迟加载、又支持高并发的单例实现方式,也就是双重检测实现方式:

在这种实现方式中,只要 instance 被创建之后,即便再调用 getInstance() 函数也不会再进入到加锁逻辑中了。所以,这种实现方式解决了懒汉式并发度低的问题。具体的代码实现如下所示:

/**
 * 双重检查锁单例的实现
 */
public class DclSingleton {

    //1.持有一个jvm全局唯一的实例
    //需要在实例对象前加上volatile关键字保证对象创建的原子性
    //volatile  1.保证内存可见  2.保证有序性
    private volatile static DclSingleton instance;

    //2.私有化构造器
    private DclSingleton() {
    }

    //3.对外提供一个获取实例的方法
    //双重检查锁只有在一个时间段内,有n个线程同时进入方法,n个线程排队拿锁,
    //等这些线程全部释放锁以后,之后的线程就不会去拿锁了,因为此时已经创建好了实例
    //大大的提高了性能
    public static DclSingleton getInstance() {
        if (instance == null) { //线程1  线程2   线程3
            synchronized (DclSingleton.class) { //线程1拿到锁
                if(instance == null){ //再次检查对象是否创建
                    //在JDK9以前,创建对象的操作并不是原子性的操作(底层可能是乱序执行),如果对象的初始化比较复杂的话
                    //它可能会处于一种半初始化状态(此时对象还未创建成功),存在线程安全问题。
                    instance = new DclSingleton();
                }
            }
        }
        return instance;
    }
}

4、静态内部类

我们再来看一种比双重检测更加简单的实现方法,那就是利用 Java 的静态内部类。它有点类似饿汉式,但又能做到了延迟加载。

public class InnerSingleton {

    //1.私有化构造器
    private InnerSingleton(){}

    //2.从内部类中获取一个单例对象
    public InnerSingleton getInstance(){  //如果没有执行这个方法,内部类就不会被加载
        return InnerSingletonHolder.instance;
    }

    //3.静态内部类持有单例对象
    //特性:类加载的时机 --> 第一次被使用的时候加载,类没有被使用则不加载
    private static class InnerSingletonHolder{
        //实例会在内部类加载
        private static final InnerSingleton instance = new InnerSingleton();
    }
}

5、枚举

这种实现方式通过 Java 枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性。


/*
* 基于枚举的单例实现
* 实现一个全局计数器
* 枚举支持懒加载,并且是线程安全的
* */
public enum GlobalCount {
    //任何一个枚举项就是一个单例
    //本质上就是 static final GlobalCount = new GlobalCount();
    INSTANCE;  //对外暴露的一个接口

    private static final AtomicLong count = new AtomicLong(0);

    public Long getCount(){
        return count.getAndIncrement();
    }
}

我们通过以上方式创建出来的单例对象还存在一些问题,比如可以通过反射的方式利用构造器创建对象,当然,枚举方式创建的单例可以避免这个问题。

我们需要进行改进,以DclSingleton为例:

//2.私有化构造器
    private DclSingleton() {
        if(instance != null){
            throw new RuntimeException("该实例只允许实例化一次");
        }
    }

但是这样还不行,我们仍然可以通过序列化的方式把对象写入一个文件中,然后再通过反序列读取文件,此时从文件中读取到的对象是一个新的实例。可以再加上一个readResolve()方法。

public Object readResolve(){
        return instance;
    }

readResolve()方法可以用于替换从流中读取的对象,在进行反序列化时,会尝试执行readResolve方法,并将返回值作为反序列化的结果,而不会克隆一个新的实例,保证jvm中仅仅有一个实例存在.

三、单例存在的问题

尽管单例是一个很经典的设计模式,但在实际的开发中,我们也很少按照严格的定义去使用它,以上的知识大多是为了理解和面试而使用和学习。

大部分情况下,我们在项目中使用单例,都是用它来表示一些全局唯一类,比如配置信息类、连接池类、ID 生成器类。单例模式书写简洁、使用方便,在代码中,我们不需要创建对象。但是,这种使用方法有点类似硬编码(hard code),会带来诸多问题,所以我们一般会使用spring的单例容器作为替代方案

1、无法支持面向对象编程

我们知道,OOP 的三大特性是封装、继承、多态。单例将构造私有化,直接导致的结果就是,他无法成为其他类的父类,这就相当于直接放弃了继承和多态的特性,也就相当于损失了可以应对未来需求变化的扩展性。

2、极难的横向扩展

我们知道,单例类只能有一个对象实例。如果未来某一天,一个实例已经无法满足我们的需求,我们需要创建一个,或者更多个实例时,就必须对源代码进行修改,无法友好扩展。

在系统设计初期,我们觉得系统中只应该有一个数据库连接池,这样能方便我们控制对数据库连接资源的消耗。所以,我们把数据库连接池类设计成了单例类。但之后我们发现,系统中有些 SQL 语句运行得非常慢。这些 SQL 语句在执行的时候,长时间占用数据库连接资源,导致其他 SQL 请求无法响应。为了解决这个问题,我们希望将慢 SQL 与其他 SQL 隔离开来执行。为了实现这样的目的,我们可以在系统中创建两个数据库连接池,慢 SQL 独享一个数据库连接池,其他 SQL 独享另外一个数据库连接池,这样就能避免慢 SQL 影响到其他 SQL 的执行。

如果我们将数据库连接池设计成单例类,显然就无法适应这样的需求变更,也就是说,单例类在某些情况下会影响代码的扩展性、灵活性。所以,数据库连接池、线程池这类的资源池,最好还是不要设计成单例类。实际上,一些开源的数据库连接池、线程池也确实没有设计成单例类。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值