「设计模式」- 教你手写单例模式

public class LazySingleton {

private static LazySingleton lazySingleton;

private LazySingleton() {

System.out.println(Thread.currentThread().getName());

}

public static LazySingleton getInstance() {

if (lazySingleton == null) lazySingleton = new LazySingleton();

return lazySingleton;

}

}

复制代码

但是这个实现在多线程的环境下是不安全的,试想以下,当 lazySingleton 为空时,试想一下,当lazySingleton 为空时,有多个线程同时通过了if (lazySingleton == null) 的判断,这样就会导致new 被执行了多次,使用代码复现一下:

public static void main(String[] args) {

for (int i = 0; i < 10; i++) {

new Thread(() -> LazySingleton.getInstance()).start();

}

}

复制代码

控制台输出:

可以看到,实例化代码被执行了三次,为了解决线程安全的问题有两个方法:

  1. getInstance() 方法的层级上加关键字 synchronized

  2. 引入双重检测锁

3.3 双重校验锁

为了解决懒汉式线程不安全的问题,可以引入双重校验锁的机制,双重检验锁也是一种延迟加载,并且较好的解决了在确保线程安全的时候效率低下的问题

以下是代码实现:

public class DCLSingleton {

private volatile static DCLSingleton dclSingleton;

private DCLSingleton() { }

public static DCLSingleton getInstance() {

if (dclSingleton == null) {

synchronized (DCLSingleton.class) {

if (dclSingleton == null) dclSingleton = new DCLSingleton();

}

}

return dclSingleton;

}

}

复制代码

在这个实现中,对比一下懒汉式在方法上加锁,那么每次调用那个方法都要获得锁,释放锁,等待等待……而双重校验锁锁住了部分的代码。进入方法如果检查为空才进入同步代码块,这样很明显效率高了很多

3.3.1为什么要双重校验

那在这里为什么 dclSingleton == null 要判断两次,假设我们先去掉第二次的判断。

如果两个线程一起调用 getInstance()方法,并且都通过了第一次的判断 dclSingleton == null,那么第一个线程获取了锁,然后进行了实例化后释放了锁,然后第二个线程会开始执行,然后马上也进行了实例化,这就尴尬了。

所以加上第二次判断后,先进来的线程判断了一下,哦?为空,我创建一个,然后创建一个实例之后释放了锁,第二个线程进来之后,哎?已经有了,那我就不用创建了,然后释放了锁,开开心心的完成了单例模式。

3.3.2 为什么要使用关键字volatile

对于 new 操作来说,它不是一个原子性操作,他在底层大概发生了以下三件事:

  • 在堆中分配内存空间

  • 执行它的构造方法,初始化对象

  • 在栈中定义引用,再把这个对象指给堆中的实际对象

我们期望它是按顺序发生的,但是由于Java的指令重排机制,可能在没有初始化对象时,就把栈中定义的引用指给堆中的空间,当第二个线程再进来的时候,第一次判定是否为空,他认为不为空,于是将还没有进行初始化的对象返回了;这就是为什么要加上关键字volatile的原因。

3.4 静态内部类实现

InnerClassSingleton类加载时,静态内部类 InnerClass没有被加载进内存。只有当调用 getInstance() 方法从而触发 InnerClass.INSTANCEInnerClass才会被加载,初始化实例 INSTANCE。

这种方式不仅具有延迟初始化的好处,而且由虚拟机提供了对线程安全的支持。

public class InnerClassSingleton {

private InnerClassSingleton() { }

public static InnerClassSingleton getInstance() {

return InnerClass.INSTANCE;

}

static class InnerClass {

private static final InnerClassSingleton

INSTANCE = new InnerClassSingleton();

}

}

复制代码

3.5 枚举

这是单例模式的最佳实践,它实现简单,并且在面对复杂的序列化或者反射攻击的时候,能够防止实例化多次

外部调用直接使用 Singleton.INSTANCE,简单粗暴。

由于 Enum 实现了 Serializable 接口,所以不用考虑序列化的问题(其实序列化反序列化也能导致单例失败的,但是我们这里不过多研究),并且加载的时候 JVM 能确保只加载一个实例,所以它是线程安全的,而且反射无法破解这种单例模式的实现

public enum Singleton {

INSTANCE;

}

复制代码

总结

本文论述了单例模式常见的五种实现方式,在《Effect Java》中,作者极力推崇使用枚举类来实现单例模式,并认为这个实现是单例模式的最佳实践

最后

Java架构进阶面试及知识点文档笔记

这份文档共498页,其中包括Java集合,并发编程,JVM,Dubbo,Redis,Spring全家桶,MySQL,Kafka等面试解析及知识点整理

image

Java分布式高级面试问题解析文档

其中都是包括分布式的面试问题解析,内容有分布式消息队列,Redis缓存,分库分表,微服务架构,分布式高可用,读写分离等等!

image

互联网Java程序员面试必备问题解析及文档学习笔记

image

Java架构进阶视频解析合集

9mVE3zoi-1714491068674)]

Java分布式高级面试问题解析文档

其中都是包括分布式的面试问题解析,内容有分布式消息队列,Redis缓存,分库分表,微服务架构,分布式高可用,读写分离等等!

[外链图片转存中…(img-ruyGmh64-1714491068674)]

互联网Java程序员面试必备问题解析及文档学习笔记

[外链图片转存中…(img-J0VddaCE-1714491068674)]

Java架构进阶视频解析合集

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

  • 30
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值