vlambda博客
学习文章列表

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

https://time.geekbang.org    极客时间-设计模式之美-笔记


单例设计模式(Singleton Design pattern)理解起来非常简单。一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。
如何实现一个单例?
要实现一个单例。我们需要关注的如下几个点:
  1. 构造函数需要是private访问权限的,这样才能避免外部通过new创建实例;

  2. 考虑对象创建时的线程安全问题;

  3. 考虑是否支持延迟加载;

  4. 考虑 getInstance() 性能是否高(是否加锁)。

1. 饿汉式

饿汉式的实现方式比较简单。在类加载的时候,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()这个方法加了锁,导致这个函数并发度很低。如果这个单例类偶尔被用到,那这种实现方式还是可以接受。但是平凡的使用,那频繁的加锁、释放锁及并发度低等问题,会导致性能瓶颈。

3. 双重检测

饿汉式不支持延迟加载,懒汉式有性能问题,不支持高并发。双重检测的实现方式会同时支持延迟加载,又支持高并发。

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); }}
多例模式跟工厂模式的不同之处是,多例模式创建的对象都是同一个类的对象,而工厂模式创建的是不同子类的对象。