【每周一文】开始设计模式之旅
开篇
单例模式(Singleton Pattern):只能有一个实例,控制创建对象的方式,只能通过一种方式去实例化;为了解决一个全局使用的类频繁地创建与销毁。
一、使用场景
- 要求生产唯一序列号;
- 数据库的连接池不会反复创建;
- spring中⼀个单例模式bean的⽣成和使⽤;
- 在我们平常的代码中需要设置全局的的⼀些属性保存
二、优缺点
优点:
- 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例;
- 避免对资源的多重占用(比如写文件操作)。
缺点:
没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
三、实现方式
- 静态类:
/**
* 单例静态类
*
* - 这种⽅式在我们平常的业务开发中⾮常场常⻅,这样静态类的⽅式可以在第⼀次运⾏的时候直接初始化Map类,
* 同时这⾥我们也不需要到延迟加载在使⽤。
* - 在不需要维持任何状态下,仅仅⽤于全局访问,这个使⽤使⽤静态类的⽅式更加⽅便。
* - 但如果需要被继承以及需要维持⼀些特定状态的情况下,就适合使⽤单例模式。
*
* @author 2021/12/5 17:06
*/
public class Singleton_0 {
public static Map<String,String> map = new ConcurrentHashMap<>();
}
- 懒汉式(线程不安全)
/**
* 懒汉式(线程不安全)
*
* - 单例模式有⼀个特点就是不允许外部直接创建,也就是new Singleton_01(),
* 因此这⾥在默认的构造函数上添加了私有属性 private。
* - ⽬前此种⽅式的单例确实满⾜了懒加载,但是如果有多个访问者同时去获取对象实例你可以想象成⼀堆⼈在抢厕所,
* 就会造成多个同样的实例并存,从⽽没有达到单例的要求。
* @author 2021/12/5 17:09
*/
public class Singleton_1 {
private static Singleton_1 singleton;
private Singleton_1(){}
public static Singleton_1 getInstance(){
if(Objects.nonNull(singleton)){
return singleton;
}
return new Singleton_1();
}
}
- 懒汉式(线程安全)
/**
* 懒汉式(线程安全)
*
* - 此种模式虽然是安全的,但由于把锁加到⽅法上后,所有的访问都因需要锁占⽤导致资源的浪费。
* 如果不是特殊情况下,不建议此种⽅式实现单例模式。
*
* @author 2021/12/5 17:13
*/
public class Singleton_2 {
private static Singleton_2 singleton;
private Singleton_2() {
}
public synchronized static Singleton_2 getInstance() {
if(Objects.nonNull(singleton)){
return singleton;
}
return singleton;
}
}
- 饿汉式(线程安全)
/**
* 饿汉式(线程安全)
*
* - 此种⽅式与我们开头的第⼀个实例化Map基本⼀致,在程序启动的时候直接运⾏加载,后续有外部需要使⽤的时候获取即可。
* - 但此种⽅式并不是懒加载,也就是说⽆论你程序中是否⽤到这样的类都会在程序启动之初进⾏创建。
* - 那么这种⽅式导致的问题就像你下载个游戏软件,可能你游戏地图还没有打开呢,
* 但是程序已经将这些地图全部实例化。到你⼿机上最明显体验就⼀开游戏内存满了,⼿机卡了,需要换了。
* @author 2021/12/5 17:15
*/
public class Singleton_3 {
private static Singleton_3 singleton = new Singleton_3();
private Singleton_3(){
};
public static Singleton_3 getInstance(){
return singleton;
}
}
- 类的内部类(线程安全)
/**
* 使用类的内部类(线程安全)
*
* - 使⽤类的静态内部类实现的单例模式,既保证了线程安全又保证了懒加载,同时不会因为加锁的⽅式耗费性能。
* - 这主要是因为JVM虚拟机可以保证多线程并发访问的正确性,也就是⼀个类的构造⽅法在多线程环境下可以被正确的加载。
* - 此种⽅式也是⾮常推荐使⽤的⼀种单例模式
* @author 2021/12/5 17:19
*/
public class Singleton_4 {
private static class InnerSingleton{
private static Singleton_4 singleton = new Singleton_4();
}
private Singleton_4(){
}
public static Singleton_4 getInstance(){
return InnerSingleton.singleton;
}
}
- 双重锁校验(线程安全)
/**
* 双重锁校验(线程安全)
*
* - 双重锁的⽅式是⽅法级锁的优化,减少了部分获取实例的耗时;
* - 同时这种⽅式也满⾜了懒加载。
* @author 2021/12/5 17:24
*/
public class Singleton_5 {
private static Singleton_5 singleton;
private Singleton_5(){
}
public static Singleton_5 getInstance(){
if(Objects.nonNull(singleton)){
return singleton;
}
synchronized (Singleton_5.class){
if(Objects.isNull(singleton)){
return new Singleton_5();
}
}
return singleton;
}
}
- CAS「AtomicReference」(线程安全)
**
* CAS「AtomicReference」(线程安全)
*
* - java并发库提供了很多原⼦类来⽀持并发访问的数据安全性;
* - AtomicInteger、AtomicBoolean、AtomicLong、AtomicReference。
* - AtomicReference 可以封装引⽤⼀个V实例,⽀持并发访问如上的单例⽅式就是使⽤了这样的⼀个特点。
* - 使⽤CAS的好处就是不需要使⽤传统的加锁⽅式保证线程安全,⽽是依赖于CAS的忙等算法,
* 依赖于底层硬件的实现,来保证线程安全。相对于其他锁的实现没有线程的切换和阻塞也就没有了额外的开销,
* 并且可以⽀持较⼤的并发性。
* - 当然CAS也有⼀个缺点就是忙等,如果⼀直没有获取到将会处于死循环中。
*
*
* @author 2021/12/5 17:29
*/
public class Singleton_6 {
private static final AtomicReference<Singleton_6> SINGLETON = new AtomicReference<>();
private Singleton_6(){}
public static final Singleton_6 getInstance(){
for (;;){
Singleton_6 singleton = SINGLETON.get();
if(Objects.nonNull(singleton)){
return singleton;
}
SINGLETON.compareAndSet(null,new Singleton_6());
return SINGLETON.get();
}
}
}
- 枚举单例(线程安全)
/**
* 枚举单例(线程安全)
*
* - Effective Java 作者推荐使⽤枚举的⽅式解决单例模式,此种⽅式可能是平时最少⽤到的。
* - 这种⽅式解决了最主要的;线程安全、⾃由串⾏化、单⼀实例。
*
* 这种写法在功能上与共有域⽅法相近,但是它更简洁,⽆偿地提供了串⾏化机制,绝对防⽌对此实例化,
* 即使是在⾯对复杂的串⾏化或者反射攻击的时候。虽然这种⽅法还没有⼴泛采⽤,
* 但是单元素的枚举类型已经成为实现Singleton的最佳⽅法。但也要知道此种⽅式在存在继承场景下是不可⽤的。
* @author 2021/12/5 17:35
*/
public enum Singleton_7 {
SINGLETON;
public void test(){
System.out.println("test");
}
}
注: 在平时的开发中如果可以确保此类是全局可⽤不需要做懒加载,那么直接创建并给外部调⽤即可。但如果是很多的类,有些需要在⽤户触发⼀定的条件后(游戏关卡)才显示,那么⼀定要⽤懒加载。线程的安全上可以按需选择。
本文章仅学习记录使用,参考资源:
《重学Java设计模式》 ——小傅哥