【设计模式】单例模式
1. 前言
单例模式可以说是整个设计中最简单的模式之⼀,⽽且这种⽅式即使在没有看设计模式相关资料也会常⽤在编码开发中。
因为在编程开发中经常会遇到这样⼀种场景,那就是需要保证⼀个类只有⼀个实例哪怕多线程同时访问,并需要提供⼀个全局访问此实例的点
1.1 用到的场景
本章节的技术所出现的场景⾮常简单也是我们⽇常开发所能⻅到的,例如:
- 数据库的连接池不会反复创建
- spring中⼀个单例模式bean的⽣成和使⽤
- 在我们平常的代码中需要设置全局的的⼀些属性保存
在我们的⽇常开发中⼤致上会出现如上这些场景中使⽤到单例模式,虽然单例模式并不复杂但是使⽤⾯却⽐较⼴。
2. 七种单例模式的实现
2.1 静态类实现
public class Singleton_00 {
public static Map<String,String> cache = new ConcurrentHashMap<String, String>();
}
- 以上这种⽅式在我们平常的业务开发中⾮常场常⻅,这样静态类的⽅式可以在第⼀次运⾏的时候直接初始化Map类,同时这⾥我们也不需要到延迟加载在使⽤。
- 在不需要维持任何状态下,仅仅⽤于全局访问,这个使⽤使⽤静态类的⽅式更加⽅便。
- 但如果需要被继承以及需要维持⼀些特定状态的情况下,就适合使⽤单例模式。
2.2 懒汉模式(线程不安全)
public class Singleton_01 {
private static Singleton_01 instance;
private Singleton_01() {
}
public static Singleton_01 getInstance(){
if (null != instance) {
return instance;
}
return new Singleton_01();
}
}
- 单例模式有⼀个特点就是不允许外部直接创建,也就是 new Singleton_01() ,因此这⾥在默认
的构造函数上添加了私有属性 private 。 - ⽬前此种⽅式的单例确实满⾜了懒加载,但是如果有多个访问者同时去获取对象实例你可以想象成
⼀堆⼈在抢厕所,就会造成多个同样的实例并存,从⽽没有达到单例的要求。
2.3 懒汉模式(线程安全)
public class Singleton_02 {
private static Singleton_02 instance;
private Singleton_02() {
}
public static synchronized Singleton_02 getInstance() {
if (null != instance) {
return instance;
}
return new Singleton_02();
}
}
- 此种模式虽然是安全的,但由于把锁加到⽅法上后,所有的访问都因需要锁占⽤导致资源的浪费。
如果不是特殊情况下,不建议此种⽅式实现单例模式。
2.4 饿汉模式(线程安全)
public class Singleton_03 {
private static Singleton_03 instance = new Singleton_03();
private Singleton_03() {
}
public static Singleton_03 getInstance() {
return instance;
}
}
- 此种⽅式与我们开头的第⼀个实例化 Map 基本⼀致,在程序启动的时候直接运⾏加载,后续有外
部需要使⽤的时候获取即可。 - 但此种⽅式并不是懒加载,也就是说⽆论你程序中是否⽤到这样的类都会在程序启动之初进⾏创
建。 - 那么这种⽅式导致的问题就像你下载个游戏软件,可能你游戏地图还没有打开呢,但是程序已经将
这些地图全部实例化。到你⼿机上最明显体验就⼀开游戏内存满了,⼿机卡了,需要换了。
2.5 使⽤类的内部类(线程安全)
public class Singleton_04 {
private static class SingletonHolder {
private static Singleton_04 instance = new Singleton_04();
}
private Singleton_04() {
}
public static Singleton_04 getInstance() {
return SingletonHolder.instance;
}
}
- 使⽤类的静态内部类实现的单例模式,既保证了线程安全有保证了懒加载,同时不会因为加锁的⽅式耗费性能。
- 这主要是因为JVM虚拟机可以保证多线程并发访问的正确性,也就是⼀个类的构造⽅法在多线程环境下可以被正确的加载。
- 此种⽅式也是⾮常推荐使⽤的⼀种单例模式。
2.6 双重锁校验(线程安全)
public class Singleton_05 {
private static volatile Singleton_05 instance;
private Singleton_05() {
}
public static Singleton_05 getInstance(){
if(null != instance) {
return instance;
}
synchronized (Singleton_05.class){
if (null == instance){
instance = new Singleton_05();
}
}
return instance;
}
}
- 双重锁的⽅式是⽅法级锁的优化,减少了部分获取实例的耗时。
- 同时这种⽅式也满⾜了懒加载。
2.7 CAS「AtomicReference」(线程安全)
public class Singleton_06 {
private static final AtomicReference<Singleton_06> INSTANCE = new AtomicReference<Singleton_06>();
private static Singleton_06 instance;
private Singleton_06() {
}
public static final Singleton_06 getInstance() {
for (; ; ) {
Singleton_06 instance = INSTANCE.get();
if (null != instance) {
return instance;
}
INSTANCE.compareAndSet(null, new Singleton_06());
return INSTANCE.get();
}
}
public static void main(String[] args) {
System.out.println(Singleton_06.getInstance()); // org.itstack.demo.design.Singleton_06@2b193f2d
System.out.println(Singleton_06.getInstance()); // org.itstack.demo.design.Singleton_06@2b193f2d
}
}
- java并发库提供了很多原⼦类来⽀持并发访问的数据安全性; AtomicInteger 、 AtomicBoolean 、 AtomicLong 、 AtomicReference 。
- AtomicReference 可以封装引⽤⼀个V实例,⽀持并发访问如上的单例⽅式就是使⽤了这样的⼀个特点。
- 使⽤CAS的好处就是不需要使⽤传统的加锁⽅式保证线程安全,⽽是依赖于CAS的忙等算法,依赖于底层硬件的实现,来保证线程安全。相对于其他锁的实现没有线程的切换和阻塞也就没有了额外的开销,并且可以⽀持较⼤的并发性。
- 当然CAS也有⼀个缺点就是忙等,如果⼀直没有获取到将会处于死循环中。
2.8 Effective Java 推荐的枚举单例(线程安全)
public enum Singleton_07 {
INSTANCE;
public void test(){
System.out.println("hi~");
}
}
- Effective Java 作者推荐使⽤枚举的⽅式解决单例模式,此种⽅式可能是平时最少⽤到的。
- 这种⽅式解决了最主要的;线程安全、⾃由串⾏化、单⼀实例。
调用方式
@Test
public void test() {
Singleton_07.INSTANCE.test();
}
这种写法在功能上与共有域⽅法相近,但是它更简洁,⽆偿地提供了串⾏化机制,绝对防⽌对此实例化,即使是在⾯对复杂的串⾏化或者反射攻击的时候。虽然这中⽅法还没有⼴泛采⽤,但是单元素的枚举类型已经成为实现Singleton的最佳⽅法。
但也要知道此种⽅式在存在继承场景下是不可⽤的。
总结
- 虽然只是⼀个很平常的单例模式,但在各种的实现上真的可以看到java的基本功的体现,这⾥包括了;懒汉、饿汉、线程是否安全、静态类、内部类、加锁、串⾏化等等。
- 在平时的开发中如果可以确保此类是全局可⽤不需要做懒加载,那么直接创建并给外部调⽤即可。但如果是很多的类,有些需要在⽤户触发⼀定的条件后(游戏关卡)才显示,那么⼀定要⽤懒加载。线程的安全上可以按需选择。