目录与学习目标
1:表示全局唯一类2:饿汉式的Id生成器3:懒汉式的Id生成器4:双重检测5:静态内部类(了解即可)6:单例模式存在的问题(了解即可)
1:表示全局唯一类
从业务概念上,如果有些数据在系统中只应保存一份,那就比较适合设计为单例类。比如,配置信息类。在系统中,我们只有一个配置文件,当配置文件被加载到内存之后,以对象的形式存在,也理所应当只有一份。再比如,唯一递增 ID 号码生成器,如果程序中有两个对象,那就会存在生成重复 ID 的情况,所以,我们应该将 ID 生成器类设计为单例。
2:饿汉式的Id生成器
饿汉式的实现方式比较简单。在类加载的时候,instance 静态实例就已经创建并初始化好了,所以,instance 实例的创建过程是线程安全的。不过,这样的实现方式不支持延迟加载(也就是在真正用到 IdGenerator 的时候,再创建实例)饿汉式的优缺点(相对而言):缺点: 如果实例占用资源多(比如占用内存多)提前初始化实例是一种浪费资源的行为。 如果初始化耗时长(比如需要加载各种配置文件),提前初始化实例会延缓服务的启动。优点1:如果实例占用资源多 如果资源不够,就会在程序启动的时候触发报错(比如 Java 中的 PermGen Space OOM), 我们可以立即去修复。这样也能避免在程序运行一段时间后,突然因为初始化这个实例占用资源过多,导致系统崩溃,2:如果初始化耗时长,那我们最好不要等到真正要用它的时候, 才去执行这个耗时长的初始化过程,这会影响到系统的性能.
public class IdGeneratorSingletonHungry { // AtomicLong是一个Java并发库中提供的一个原子变量类型 // 它将一些线程不安全需要加锁的复合操作封装为了线程安全的原子操作 // 比如下面会用到的incrementAndGet(). private AtomicLong id = new AtomicLong(0); private static final IdGeneratorSingletonHungry instance = new IdGeneratorSingletonHungry(); public static IdGeneratorSingletonHungry getInstance() { return instance; } public long getId() { return id.incrementAndGet(); }}
3:懒汉式的Id生成器
懒汉式相对于饿汉式的优势是支持延迟加载。不过懒汉式的缺点也很明显,我们给 getInstance() 这个方法加了一把大锁(synchronzed),导致这个函数的并发度很低。如果频繁地用到,那频繁加锁、释放锁及并发度低等问题,会导致性能瓶颈,这种实现方式就不可取了。
public class IdGeneratorSingletonLazy { private AtomicLong id = new AtomicLong(0); //初始化 instance 为空 private static IdGeneratorSingletonLazy instance; private IdGeneratorSingletonLazy() { } //直接对该方法加锁 public static synchronized IdGeneratorSingletonLazy getInstance() { if (instance == null) { instance = new IdGeneratorSingletonLazy(); } return instance; } public long getId() { return id.incrementAndGet(); }}
4:双重检测
饿汉式不支持延迟加载,懒汉式有性能问题,不支持高并发。那我们再来看一种既支持延迟加载、又支持高并发的单例实现方式,也就是双重检测实现方式。只要 instance 被创建之后,即便再调用 getInstance() 函数也不会再进入到加锁逻辑中了。所以,这种实现方式解决了懒汉式并发度低的问题。因为指令重排序,可能会导致 IdGenerator 对象被 new 出来,并且赋值给 instance 之后,还没来得及初始化;
关于Java对象创建时的步骤其实聚焦于new这个关键字,举例说明: Person person = new Person();正因为new 调用Person()构造方法 与 将内存地址赋值给变量preson没有任何关联,只要开辟了内存空间了,那么步骤2与步骤3的执行顺序就无所谓了 所以才会有可能发生指令重排。1: new 首先到堆内存开辟了一个空间 2:new 调用Person()构造方法(初始化)3:new 返回空间地址并赋值给 变量person
public class IdGeneratorSingletonDoubleCheckLock { private AtomicLong id = new AtomicLong(0); //volatile 防止指令重排 private static volatile IdGeneratorSingletonDoubleCheckLock instance; private IdGeneratorSingletonDoubleCheckLock() { } public static IdGeneratorSingletonDoubleCheckLock getInstance() { if (instance == null) { // 此处为类级别的锁 synchronized (IdGeneratorSingletonDoubleCheckLock.class) { if (instance == null) { instance = new IdGeneratorSingletonDoubleCheckLock(); } } } return instance; } public long getId() { return id.incrementAndGet(); }}
5:静态内部类(了解即可)
再来看一种比双重检测更加简单的实现方法,那就是利用 Java 的静态内部类。它有点类似饿汉式,但又能做到了延迟加载。SingletonHolder 是一个静态内部类,当外部类 IdGenerator 被加载的时候,并不会创建 SingletonHolder 实例对象。只有当调用 getInstance() 方法时,SingletonHolder 才会被加载,这个时候才会创建 instance。instance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。所以,这种实现方法既保证了线程安全,又能做到延迟加载。
public class IdGeneratorSingletonInnerClass { private AtomicLong id = new AtomicLong(0); private IdGeneratorSingletonInnerClass() { } private static class SingletonHolder { //饿汉式 创建单例 private static final IdGeneratorSingletonInnerClass instance = new IdGeneratorSingletonInnerClass(); } //懒汉式 加载 当外部类 IdGenerator 被加载的时候,并不会创建 SingletonHolder 实例对象 public static IdGeneratorSingletonInnerClass getInstance() { return SingletonHolder.instance; } public long getId() { return id.incrementAndGet(); }}
6:单例模式存在的问题(了解即可)
1:单例对 OOP 特性的支持不友好 我们知道,OOP 的四大特性是封装、抽象、继承、多态。 单例这种设计模式对于其中的抽象、继承、多态都支持得不好,非常难以实现2:单例会隐藏类之间的依赖关系 单例类不需要显示创建、不需要依赖参数传递,在函数中直接调用就可以了。如果代码比较复杂,这种调用关系就会非常隐蔽。3:单例对代码的扩展性不友好 单例类只能有一个对象实例。如果未来某一天,我们需要在代码中创建两个实例或多个实例,那就要对代码有比较大的改动。 例如: 系统中只应该有一个数据库连接池,这样能方便我们控制对数据库连接资源的消耗。 但系统中有些 SQL 语句运行得非常慢,我们需要把慢 SQL 与其他 SQL 隔离开来执行。 此时系统中创建两个数据库连接池,慢 SQL 独享一个数据库连接池,其他 SQL 独享另外一个数据库连接池。
项目连接
请配合项目代码食用效果更佳:项目地址:https://github.com/hesuijin/hesujin-design-patternGit下载地址:https://github.com.cnpmjs.org/hesuijin/hesujin-design-pattern.gitdemo-study模块 下 build_design_pattern singleton包下 idGeneratorDemo包