设计模式之单例模式
来源自《重学Java设计模式》链接提取码:ytc3
单例模式
单例模式可以说是整个设计中最简单的模式之一,而且这种方式即使在没有看设计模式相关资料也会常用在编码开发中。
因为在编程开发中经常会遇到这样的一种场景,那就是需要保证一个类只有一个实例哪怕多线程同时访问,并需要提供一个全局访问此实例的点。
综上以及我们平常的开发中,可以总结一条经验,单例模式主要解决的是,一个全局使用的类频繁的创建和消费,从而提升整体代码的性能。
案例场景
有很多日常开发中所见到的
- 数据库的连接池不会反复创建。
- spring中一个单例模式bean的生成和使用。
- 在我们平常的代码中需要设置全局的一些属性值保存。
七种单例模式实现
单例模式的实现方法比较多,主要在实现上是否支持懒汉模式,是否线程安全中运用各项技巧。当然也有一些场景不需要考虑懒加载也就是懒汉模式的情况,会直接使用static
静态类或属性和方法的方式进行处理,供外部使用。
接下来我们就通过实现不同方式的实现进行讲解单例模式。
0. 静态类使用
public class Singleton_00 {
public static Map<String,String> cache = new ConcurrentHashMap<String, String>();
}
- 以上这种方法在我们平常的业务开发中非常常见,这样静态类的方式可以在第一次进行类加载时直接初始化Map类,同时这里我们也不需要用到延迟加载的作用。
- 在不需要维持任何状态下,仅仅用于全局访问,这个使用静态类的方式更加方便。
- 但如果需要被继承以及需要维持一些特定的情况下,就适合使用单例模式。
1. 懒汉模式(线程不安全)
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. 懒汉模式(线程安全)
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();
}
}
- 这种模式虽然是安全的,但由于使用了
synchronized
关键字,并且将其加在了方法上,(这是一个方法级锁,一般来说锁的粒度越小,意味者这个锁的性能越好。),所有的方法都因需要锁占用导致资源的浪费。如果不是特殊情况下,不建议此种方法实现单例模式。
3. 饿汉模式(线程安全)
public class Singleton_03 {
private static Singleton_03 instance = new Singleton_03();
private Singleton_03() {
}
public static Singleton_03 getInstance() {
return instance;
}
}
- 此种方法与我们开头的第一个实例化
Map
基本一致,在程序启动的时候直接运行加载,后续有外部需要使用的时候获取即可。 - 但此种方式并不是懒加载,也就是说无论你程序中是否用到这样的类都会在程序启动时进行创建。
- 那么这种方式导致的问题就像你下载一个游戏软件,可能你地图还没打开呢,但是程序已经将这些地图全部实例化。到你手机上最明显的体验就是一开游戏内存满了,手机卡了。
4.使用静态内部类(线程安全)
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虚拟机在类加载阶段的线程安全性都是由JVM来保证的,这也就是一个类的构造函数在多线程环境下可以被正确的加载。
- 此种方式也是非常推荐使用的一种单例模式。
5.双重锁校验(线程安全)
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;
}
}
- 双重校验锁的方式是方法级锁的优化,将方法级锁改为同步代码块,(此方法注意改善线程安全性问题使用了
volatile
关键字),减少了部分获取实例的耗时。 - 同时这种方式也满足了懒加载
6.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());
System.out.println(Singleton_06.getInstance());
}
}
- java并发库提供了很多原子类来支持并发访问的数据安全性,
AtomicInteger
、AtomicBoolean
、AtomicLong
、AtomicReference
。 - AtomicReference可以封装引用一个V实例,支持并发访问如上的单例方式就是使用了这样一个特点。
- 使用CAS的好处就是不需要使用传统的加锁方式保证线程安全,而是依赖CAS的忙等算法,依赖于底层硬件的实现来保证线程安全。相对于其他锁的实现没有线程的切换和阻塞也就没有额外的开销,并且可以支持较大的并发性。
- 当然CAS也有一个缺点就是忙等,如果一直没有获取到将会处于死循环中。
7. 使用枚举型单例
public enum Singleton_07 {
INSTANCE;
public void test(){
System.out.println("hi~");
}
}
- Effective Java作者推荐使用枚举的方式解决单例模=模式,此种方式可能是平时最少用到的。
- 这种方式解决了最主要的:线程安全,自由串行化,单例实例。
调用方式
@Test
public void test() {
Singleton_07.INSTANCE.test();
}
这种写法在功能上与公有域方法相近,但是它更简洁,无偿地提供了串行化机制,绝对防止对此实例化,即使是在面对复杂地串行化或者反射攻击地时候。虽然这种方法还没有广泛采用,但是单元素地枚举类型已经成为实现Singleton地最佳方法。
但是也要知道此种方法在存在继承场景下是不可用的。
总结
- 虽然只是一个很平常的单例模式,但是在各种实现上真的可以看到Java基本功的体现,这里包括了:懒汉、饿汉、线程是否安全、静态类、内部类、加锁、序列化等等。
- 在平时的开发中如果可以确保此类是全局可用不需要做懒加载,那么直接创建并给外部调用即可。但如果是很多的类,有些需要在用户触发一定的条件后(游戏关卡)才显示,那么一定要使用懒加载。线程安全上可以按需选择。
评论区