一、单例模式介绍
1. 解决的问题
- 保证一个类只有一个实例。 最常见的是控制某些共享资源 (例如数据库或文件) 的访问权限。
运作方式是这样的: 如果创建了一个对象,同时过一会儿后决定再创建一个新对象,此时会获得之前已创建的对象, 而不是一个新对象。
注意, 普通构造函数无法实现上述行为,因为构造函数的设计决定了它必须总是返回一个新对象。
- 为该实例提供一个全局访问节点。 还记得用过的那些存储重要对象的全局变量吗?它们在使用上十分方便,但同时也非常不安全,因为任何代码都有可能覆盖掉那些变量的内容,从而引发程序崩溃。
和全局变量一样, 单例模式也允许在程序的任何地方访问特定对象。同时可以保护该实例不被其他代码覆盖。
2. 定义
单例模式是一种创建型设计模式,能够保证一个类只有一个实例,并提供一个访问该实例的全局节点。
3. 应用场景
- 程序中的某个类对于所有客户端只有一个可用的实例,可以使用单例模式。
- 要更加严格地控制全局变量,可以使用单例模式。
注意:可以随时调整限制并设定生成单例实例的数量,只需修改 获取实例
方法,即 getInstance
中的代码即可实现。
4. 与其他设计模式的关系
- 抽象工厂模式、生成器模式和原型模式都可以用单例来实现。
- 外观模式类通常可以转换为单例模式类,因为在大部分情况下一个外观模式就足够了。
如果能将对象的所有共享状态简化为一个享元对象,那么享元模式就和单例模式类似。但这个两个设计模式有两个根本性的不同。
- 只会有一个单例实体,但享元类可以有多个实体,各实体的内在状态也可以不同。
- 单例对象可以是可变的。享元对象是不可变的。
二、单例模式优缺点
1. 优点
- 可以保证一个类只有一个实例。
- 获得了一个指向该实例的全局访问节点。
- 仅在首次请求单例对象时对其进行初始化。
2. 缺点
- 违反了单一职责原则。
- 单例模式可能掩盖不良设计,比如程序各组件之间相互了解过多等。
- 该模式在多线程环境下需要进行特殊处理,避免多个线程多次出创建单例对象。
- 单例的客户端代码单元测试可能会比较困难,因为许多测试框架以基于继承的方式创建模拟对象。由于单例类的构造函数是私有的,而且绝大部分语言无法重写静态方法,所以需要仔细考虑模拟单例的方法。
三、7 种单例模式实现
静态类使用
/**
* 静态类单例实现
*/
public class StaticSingleton {
public static Map<String,String> hashMap = new ConcurrentHashMap<String, String>();
}
- 这种方式在我们的业务开发场景中非常常见,这样的方式可以在第一次运行的时候直接初始化 Map 类。
- 在不需要维持任何状态,不需要延迟加载,仅仅用于全局访问时,这种方式更便于使用。
- 但在需要被继承、需要维持一些特定状态时,单例模式更适合。
单例模式的实现方式比较多,主要是实现上是否支持懒汉模式、是否线程安全。
接下来通过不同的单例模式实现方式来解析单例模式。
1. 懒汉单例模式(线程不安全)
/**
* 懒汉单例模式(线程不安全)
*/
public class SlobThreadUnsafeSingleton {
private static SlobThreadUnsafeSingleton instance;
/**
* 单例的构造函数必须永远是私有类型,以防止使用`new`运算符直接调用构造方法
*/
private SlobThreadUnsafeSingleton() {
}
public static SlobThreadUnsafeSingleton getInstance() {
if (ObjectUtils.isEmpty(instance)) {
instance = new SlobThreadUnsafeSingleton();
}
return instance;
}
}
这种懒汉单例模式满足了懒加载,但当多个访问者同时获取对象实例时(多进程),会导致多个实例并存,没有达到单例模式的需求。
2. 懒汉单例模式(线程安全)
/**
* 懒汉单例模式(线程安全)
*/
public class SlobThreadSafeSingleton {
private static SlobThreadSafeSingleton instance;
/**
* 单例的构造函数必须永远是私有类型,以防止使用`new`运算符直接调用构造方法
*/
private SlobThreadSafeSingleton() {
}
public static synchronized SlobThreadSafeSingleton getInstance() {
if (ObjectUtils.isEmpty(instance)) {
instance = new SlobThreadSafeSingleton();
}
return instance;
}
}
这种懒汉单例模式是线程安全的,但由于锁在方法上,所有的访问都需要锁占用,会导致资源的浪费。非特殊情况,不建议使用此种方式来实现单例模式。
3. 饿汉单例模式(线程安全)
/**
* 饿汉单例模式(线程安全)
*/
public class EagerSingleton {
private static EagerSingleton instance = new EagerSingleton();
/**
* 单例的构造函数必须永远是私有类型,以防止使用`new`运算符直接调用构造方法
*/
private EagerSingleton() {
}
private static EagerSingleton getInstance() {
return instance;
}
}
饿汉单例模式并不是懒加载,简单来说就是无论是否用到该类都会在程序启动之初创建。这种方式会导致的问题类似一打开某个软件,手机直接卡死(开启了过多无用功能,导致内存不足)。
4. 双重校验锁单例模式(线程安全)
/**
* 双重校验锁单例模式(线程安全)
*/
public class DoubleCheckingLockingSingleton {
private static volatile DoubleCheckingLockingSingleton instance;
/**
* 单例的构造函数必须永远是私有类型,以防止使用`new`运算符直接调用构造方法
*/
private DoubleCheckingLockingSingleton() {
}
public static DoubleCheckingLockingSingleton getInstance() {
if (ObjectUtils.isNotEmpty(instance)) {
return instance;
}
synchronized (DoubleCheckingLockingSingleton.class) {
if (ObjectUtils.isEmpty(instance)) {
instance = new DoubleCheckingLockingSingleton();
}
}
return instance;
}
}
双重校验锁方式实现的单例模式是方法级锁的优化,减少了部分获取实例的耗时,同时也满足了懒加载。
5. 静态内部类单例模式(线程安全)
/**
* 静态内部类单例模式(线程安全)
*/
public class StaticInnerSingleton {
private static class SingletonHolder {
private static final StaticInnerSingleton INSTANCE = new StaticInnerSingleton();
}
/**
* 单例的构造函数必须永远是私有类型,以防止使用`new`运算符直接调用构造方法
*/
private StaticInnerSingleton(){
}
public static final StaticInnerSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
静态内部类实现的单例模式,既保证了线程安全,也保证了懒加载,同时不会因为加锁的方式导致性能开销过大。
这主要是因为 JVM 虚拟机可以保证多进程并发访问的正确性,即一个类的构造方法在多线程环境下,可以被正确加载。
因此,静态类内部类的实现方式非常推荐使用。
6. CAS「AtomicReference」单例模式(线程安全)
/**
* CAS「AtomicReference」单例模式(线程安全)
*/
public class CompareAndSwapSingleton {
private static final AtomicReference<CompareAndSwapSingleton> INSTANCE = new AtomicReference<CompareAndSwapSingleton>();
private static CompareAndSwapSingleton instance;
/**
* 单例的构造函数必须永远是私有类型,以防止使用`new`运算符直接调用构造方法
*/
private CompareAndSwapSingleton() {
}
public static final CompareAndSwapSingleton getInstance() {
for (; ; ) {
CompareAndSwapSingleton instance = INSTANCE.get();
if (ObjectUtils.isNotEmpty(instance)) {
return instance;
}
INSTANCE.compareAndSet(null, new CompareAndSwapSingleton());
return INSTANCE.get();
}
}
}
Java 并发库提供了很多原子类来支持并发访问的数据安全性:AtomicInteger
、 AtomicBoolean
、 AtomicLong
、AtomicReference
。
AtomicReference
可以封装引用一个实例,支持并发访问,该单例方式就是使用该特点实现。
采用 CAS 方式的优点就是不需要传统的加锁方式来保证线程安全,而是依赖 CAS 的忙等算法,依赖底层硬件的实现,来保证线程安全。相对于其他锁的实现没有线程的切换和阻塞,也就没有了额外开销,因此可以支持较大的并发。
当然,CAS 方式也存在缺点,忙等即如果一直没有获取到将会处于死循环中。
7. 枚举单例模式(线程安全)
/**
* 枚举单例(线程安全)
*/
public enum EnumSingleton {
INSTANCE;
public void whateverMethod() {
System.out.println("enum singleton method");
}
}
约书亚·布洛克(英语:Joshua J. Bloch,1961年8⽉28⽇-),美国著名程序员,《Effective Java》作者。他为Java平台设计并实作了许多的功能,曾担任Google的⾸席Java架构师(Chief Java Architect)。
《Effective Java》作者约书亚·布洛克推荐使用枚举的方式解决单例模式,这种方式应该是平时最少用到的。
这种方式解决的单例模式的主要问题:线程安全、自由串行化、单一实例。
调用方式
@Test
public void testEnumSingleton()
{
EnumSingleton.INSTANCE.whateverMethod();
}
这种写法在功能上与共有域⽅法相近,但是它更简洁,⽆偿地提供了串⾏化机制,绝对防⽌对此实例化,即使是在⾯对复杂的串⾏化或者反射攻击的时候。虽然这种方式还没有⼴泛采⽤,但是单元素的枚举类型已经成为实现单例 的最佳⽅法。
但这种⽅式在存在继承场景下是不可⽤的。
四、单例模式结构
- 单例(Singleton)类声明了一个名为
getInstance
获取实例的静态方法来返回其所属类的一个相同实例。 - 单例的构造模式必须对客户端(Client)代码隐藏。调用
获取实例
方法必须是获取单例对象的唯一方式。
设计模式并不难学,其本身就是多年经验提炼出的开发指导思想,关键在于多加练习,带着使用设计模式的思想去优化代码,就能构建出更合理的代码。
源码地址:https://github.com/yiyufxst/d...参考资料:
小博哥重学设计模式:https://github.com/fuzhengwei...
深入设计模式:https://refactoringguru.cn/de...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。