设计模式之美(2)-创建型-单例模式

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

如何实现一个单例?
要实现一个单例。我们需要关注的如下几个点:
构造函数需要是private访问权限的,这样才能避免

  1. 如何实现一个单例?
    要实现一个单例。我们需要关注的如下几个点:
    1.外部通过new创建实例;
    2.考虑对象创建时的线程安全问题;
    3.考虑是否支持延迟加载;
    4.考虑 getInstance() 性能是否高(是否加锁)

  2. 饿汉式

饿汉式的实现方式比较简单。在类加载的时候,instance静态实例就已经创建并初始化好了,所以instance实例的创建过程是线程安全的。不过这样的实现方式不支持延迟加载。

public class IdGenerator {
    private AtomicLong id = new AtomicLong();
    private static final IdGenerator idGenerator = new IdGenerator();
    private IdGenerator(){}
    public static IdGenerator getInstance(){
        return idGenerator;
    }
    public long getId(){
        return id.incrementAndGet();
    }
}

有人认为这种实现方式不好,因为不支持延迟加载,如果占用资源多或初始化耗时长,提前初始化会造成资源浪费。
但是如果初始化耗时长,那如果等到使用它的时候,才去执行这个耗时长的初始化过程,会影响到系统性能。采用饿汉式实现方式,将耗时的初始化操作,提前到程序启动的时候完成,这样能避免在程序运行的时候,再去初始化导致的性能问题。
如果实例占用资源多,提早暴露问题,也能第一时间排查修复问题。不会在程序运行一段时间后,突然初始化导致系统崩溃,影响系统的可用性。
2. 懒汉式
有饿汉式,对应的,就有懒汉式。懒汉式相对于饿汉式的优势是支持延迟加载。

public class IdGeneratorV2 {
    private AtomicLong atomicLong = new AtomicLong(0);
    private static IdGeneratorV2 idGeneratorV2;
    private IdGeneratorV2(){}
    public static synchronized IdGeneratorV2 getInstance(){
        if(idGeneratorV2 == null){
            idGeneratorV2 = new IdGeneratorV2();
        }
        return idGeneratorV2;
    }
    public long getId(){
        return atomicLong.incrementAndGet();
    }
}

懒汉式的缺点也是很明显,我们给getinstance()这个方法加了锁,导致这个函数并发度很低。如果这个单例类偶尔被用到,那这种实现方式还是可以接受。但是平凡的使用,那频繁的加锁、释放锁及并发度低等问题,会导致性能瓶颈。

  1. 双重检测
    饿汉式不支持延迟加载,懒汉式有性能问题,不支持高并发。双重检测的实现方式会同时支持延迟加载,又支持高并发。
public class IdGeneratorV3 {
    private AtomicLong atomicLong = new AtomicLong(0);
    private IdGeneratorV3(){}
    private static IdGeneratorV3 idGeneratorV3;
    public static IdGeneratorV3 getInstance(){
        if(idGeneratorV3 == null){
            synchronized (idGeneratorV3.getClass()){
                if(idGeneratorV3 == null) {
                    idGeneratorV3 = new IdGeneratorV3();
                }
            }
        }
        return idGeneratorV3;
    }
    public long getId(){
        return atomicLong.incrementAndGet();
    }
}

网上有人说,这种实现方式有些问题。因为指令重排序,可能会导致IdGeneratorV3对象被new出来,并赋值给instance之后,还没来得及初始化,就被另一个线程使用。
要解决这个问题,需要给idGeneratorV3成员变量加上volatile关键字,禁止指令重排序,实际上,只有低版本的Java才会有这个问题。我们现在用的高版本的Java已经在JDK内部实现中解决了这个问题。
4. 静态内部类
比双重检查更简单的实现方法,利用Java的静态内部类。它有点类似饿汉式,但又能做到了延迟加载。

public class IdGeneratorV4 {
    private AtomicLong atomicLong = new AtomicLong(0);
    private IdGeneratorV4(){}
    private static class SingletonHolder{
        private static final IdGeneratorV4 ID_GENERATOR_V_4 = new IdGeneratorV4();
    }
    public static IdGeneratorV4 getInstance(){
        return SingletonHolder.ID_GENERATOR_V_4;
    }
    public long getID(){
        return atomicLong.incrementAndGet();
    }
}

SingletonHolde是静态内部类,当外部类IdGeneratorV4被加载的时候,并不会创建SingletonHolde实例对象。只有当调用getInstance()方法时,SingletonHolde才会被加载,这个时候才会去创建IdGeneratorV4类实例,IdGeneratorV4的唯一性、创建过程的线程安全,都有JVM来保证。所以这种实现方式既保证了线程安全,又能做到延迟加载。
5. 枚举
枚举的实现方式通过Java枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性。

public enum IdGeneratorV5 {
    INSTANCE;
    private AtomicLong atomicLong = new AtomicLong(0);
    public long getID(){
        return atomicLong.incrementAndGet();
    }
}

单例存在哪些问题?

  1. 单例对OOP特性的支持不友好
    OOP的四大特性是封装、抽象、继承、多态。单例模式对于其中的抽象、继承、多态都支持的不好。
    单例的使用方式违背了基于接口而非实现的设计原则,违背了广义上理解的OOP的抽象特性,如果针对业务采用不同的ID生成策略。为了应对需求的变化,就需要修改所有用到IdGenerator类的地方,代码改动比较大。
    理论上单例类也可以被继承,实现多态,不过实现起来会非常的奇怪,导致可读性变差。所以一旦选择把类设计成单例类,也就意味着放弃了继承和多态这两个特性。
  2. 单例会隐藏类之间的依赖关系
    通过构造函数、参数传递等方式声明类之间的依赖关系,但是单例类不需要显示创建、不需要依赖参数传递,在函数中直接调用就可以。如果代码复杂,那这种调用关系就会非常隐蔽。
  3. 单例对代码的扩展性不友好
    单例类只有一个对象实例,如果未来某一天,需要在代码中创建两个实例或者多个实例,那就要改动比较多的代码。
    例如数据库连接池,在系统初期,系统中只有一个数据库连接池,所以把数据库连接池类设计成了单例。随着时间推移,有一些SQL非常耗时,希望将慢SQL与其他SQL隔离开来执行。为了实现这个目的,可以在系统中创建两个数据库连接池,慢SQL独享一个数据库连接池,其他SQL独享另外的,避免慢SQL影响其他SQL。
  4. 单例对代码的可测试性不友好
    单例模式的使用会影响到代码的可测试性。如果单例类依赖比较重的外部资源,比如DB,在写单元测试的时候,希望能通过mock的方式将它替换掉。而单例类这种硬编码的方式,导致无法实现mock替换。
  5. 单例不支持有参数的构造函数
    单例不支持有参数的构造函数,比如我们创建一个连接池的单例对象,我们没法通过参数来指定连接池的大小。
    代替解决方案
    为了保证全局唯一,除了使用单例,还可以使用静态方法来实现。不过静态方法这种实现思路,并不能解决我们之前遇到的问题,如果要完全解决这些问题,可以通过工厂模式,IOC容器来保证。

如何实现一个多例模式?
“单例”指的是,一个类只能创建一个对象。对应地,“多例”指的就是,一个类可以创建多个对象,但是个数是有限制的,比如只能创建3个对象。

public class BackendServer {
    public BackendServer(Integer serverNo, String serverAddress) {
        this.serverNo = serverNo;
        this.serverAddress = serverAddress;
    }

    private Integer serverNo = 3;
    private String serverAddress;
    private static final int SERVER_COUNT=3;
    private static final Map<Integer, BackendServer> BACKEND_SERVER_MAP = Maps.newHashMap();
    static {
        BACKEND_SERVER_MAP.put(1, new BackendServer(1, "127.0.0.1"));
        BACKEND_SERVER_MAP.put(2, new BackendServer(2, "127.0.0.2"));
        BACKEND_SERVER_MAP.put(3, new BackendServer(3, "127.0.0.3"));
    }
    public BackendServer getBackend(Integer serverNo){
        return BACKEND_SERVER_MAP.get(serverNo);
    }
    public BackendServer getRandomBackend(Integer serverNo){
        Random random = new Random();
        int i = random.nextInt(SERVER_COUNT)+1;
        return BACKEND_SERVER_MAP.get(i);
    }
}

多例模式跟工厂模式的不同之处是,多例模式创建的对象都是同一个类的对象,而工厂模式创建的是不同子类的对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

就是不掉头发

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值