单例模式
一、概述
单例设计模式:一个类只允许创建一个对象或者实例(唯一的范围:进程),那这个类就是一个单例类,这种设计模式即为单例设计模式。
为什么需要单例模式 ?
1、用于标识全局唯一的类(配置类、连接池类、ID生成器) 2、解决资源访问冲突的问题
为什么不用静态方法而用单例模式?
两个都能实现我们加载的目的,静态方法基于对象的,单例模式是面向对象的。一个方法和所在的实例对象无关,那么它可以为静态方法。当方法与实例有关,但是在创建类时又只需要一份实例,则需要单例模式。(如电商中的类包含一些配置和属性并且是公共的,所以只需要一份就可以了)
二、实现
需要注意的点:
- 构造函数需要被 private 访问修饰符修饰,避免外部通过 new 关键字创建实例
- 对象创建时的线程安全问题
- 是否需要延时加载
- 考虑 getInstance() 性能是否高(是否加锁)
2.1 饿汉式
instance 静态实例在类加载时已经创建并初始化好了
1 |
|
2.2 懒汉式
相比于饿汉式,懒汉式的优势是支持延迟加载(当真正需要实例时,创建实例),懒汉式需要在方法上进行加锁(不加锁会出现并发问题),并发度为 1 ,如果频繁的用到这个单例,就会频繁加锁和释放锁等问题,这种方式则不可取。
1 |
|
2.3 双重检测
懒汉式的问题在于性能问题,不支持高并发问题。通过双重检测,只要 instance 被创建后,再调用 getInstance() 函数不会进入锁逻辑中,解决了懒汉式并发度低的问题。
1 |
|
对于实例变量需要使用 volatile 关键字修饰,原因:创建一个对象分为三步:1. 分配内存空间 2. 初始化内存空间 3. 将内存地址赋值给变量,由于指令重排可能顺序变为:1—> 3 —> 2 ,导致一个线程拿到未初始化的对象,所以需要加 volatile 禁止指令重排,并且可以保证多个线程间该对象的准确性。
2.4 静态内部类
利用 Java 的静态内部类,当外部类 IdGenerator 被加载时,并不会创建 Singleton 实例对象,只有当调用 getInstance() 方法时,IdGenerator 会被创建实例,实例创建过程的线程安全性,由 JVM 保证。
1 |
|
2.5 枚举
通过 Java 枚举本身的性质,保证了实例的线程安全性和实例的唯一性。
1 |
|
三、存在的问题
- 对OOP(封装、抽象、继承、多态)特性不友好
单例模式违背了基于接口而非实现的设计原则(当需要两个独立的ID生成器时,就会产生较大的变动)
单例会隐藏类之间的依赖关系
对扩展不友好(当代码中需要多个实例时,代码改动较大)
单例不支持有参数的构造函数
要解决以上问题,可以通过工程模式、IOC容器、程序员自己保证来解决。