剑指Offer-2-实现Singleton模式
这一题在 力扣 题单里面也没有,因为不是算法相关
单例模式
常用模式中唯一一个能够用短短几十行代码完整实现的模式
即保持全局始终仅有一个对象实例,避免了频繁创建实例的开销;同时提供一个全局访问入口来实现获取唯一实例
默认情况下,Spring 容器所管理的所有对象都是单例的
实现要点
- 由于要求只能设置一个实例,把构造函数设为私有以禁止他人创建实例
- 由于外部无法创建实例,而是由内部创建唯一实例,于是需要给外部提供一个
public
方法,用于获取实例 - 确保只有一个实例,因为实例的创建完全是内部控制的,所以其实
static
并不是必要的,但是加上有隐性的好处
我们知道
static
是类相关的变量,只能有一个,进一步确保了单例,也更严谨
- 获取实例的公开方法也是
static
,这是必要的,不然就是不创建对象就无法获得单例,同时创建方法私有根本无法创建对象,自相矛盾
那么能不能用static
修饰类?将其变为一个静态类
不可以,静态类只能是内部类
那么 带静态方法的普通类 呢?这个看情况,具体见二者的辨析
C++
推荐的解法1:静态构造函数
public sealed class Singleton{ private Singleton(){} // 私有构造方法 private static Singleton instance = new Singleton(); // 实例化了(唯一)一个对象,它是私有的 public static Singleton Instance{ get{ retrun instance; } } }
静态构造函数会在类型第一次被使用的时候被调用
如果没用调用该属性,而是调用了类型的某个静态方法(不需要实例),同样会过早地创建实例
推荐的解法2
public sealed class Singleton{ private Singleton(){} // 私有构造方法 public static Singleton Instance{ get{ retrun Nested.instance; } } class Nested{ static Nested(){} internal static readonly Singleton instance = new Singleton(); } }
内部定义了一个私有类型Nested
如果不调用属性Singleton.Instance,就不会触发调用Nested,也不会创建实例
其实就是说私有类型不会像属性一样在类型第一次被使用自动调用静态构造函数的时候被创建
Java
饿汉和懒汉的区别仅在于:实例化单例对象的时机
饿汉式
类一旦加载,就把单例初始化完成,保证
getInstance()
的时候,单例是已经存在的了
public class Singleton { private Singleton() {} // 私有构造方法,保证外界无法直接实例化。 private static Singleton instance = new Singleton(); // 通过公有的静态方法获取对象实例 public static Singleton getInstace() { return instance; } }
懒汉式
只有当调用
getInstance()
的时候,才回去初始化这个单例
public class Singleton { // 私有构造方法,保证外界无法直接实例化。 private Singleton() {} private static Singleton instance = null; // 通过公有的静态方法获取对象实例 public static Singleton getInstace() { if (instance == null) { instance = new Singleton(); } return instance; } }
但是这个实现只适用于单线程环境,即这是线程不安全的
多线程下线程安全的实现倒是很简单粗暴,就是在方法前面加上synchronized
关键字
我在想有没有像《剑指Offer》上推荐的一样,更高级的写法
多线程并发双重锁
public class Singleton { private volatile static Singleton instance; public static Singleton getInstance(){ if(instance==null){ synchronized (Singleton.class){ if(instance==null) instance = new Singleton(); } } return instance; } }
为什么要这么写?
为什么要synchronzied
代码块包裹?
首先,比如两个线程同时尝试获取单例,又同时发现是null
然后去创建对象结果就不是单例了,所以要用synchronzied
代码块包裹
为什么还要再判断一次instance==null
?
多个线程同时通过了第一次检查 instance 是否为 null,然后进入同步块。其中一个线程获取到锁,实例化了对象并赋值给 instance,然后释放锁。而其他线程在获得锁后,由于没有第二次检查 instance 是否为 null,它们将再次创建新的实例,导致产生多个实例
为什么还要vloatile
关键字?
指令重排序问题:在实例化对象的过程中,可能会进行指令重排序,例如先分配内存空间然后再进行初始化。这种情况下,如果一个线程在第一次检查 instance 时发现不为 null,但实际上还没有完成对象的初始化,其他线程在获取到锁后直接使用这个尚未完全初始化的实例,会导致不正确的行为。
从原子性的角度来看,一个对象的创建通常可以分为以下几个步骤:
- 分配内存空间:
首先需要在堆内存中分配足够的空间来存储对象的数据和状态。这个步骤是一个原子操作,不会被中断或交错执行。- 初始化对象:
在分配内存后,需要对对象进行初始化,包括设置对象的默认值、执行构造函数等。这些初始化操作可能涉及到多个字段的赋值和其他逻辑。在初始化过程中,可能会有多个操作步骤,这些步骤在整个过程中应该是原子性的,不会被中断或交错执行。- 将引用指向分配的内存空间:
最后一步是将对象的引用指向分配的内存空间,使得对象可以被访问和使用。这个操作也应该是原子性的,不会被中断或交错执行。
在上述双重检查锁定的单例模式中,如果没有适当的同步机制或指令重排的处理,可能会导致以下情况:
分配内存空间和初始化对象的顺序被重排:在某些情况下,分配内存空间的操作可能被重排到对象初始化之后。这意味着,一个线程可以在分配内存空间之前获取到对象的引用,但实际上该对象可能还没有初始化。在这种情况下,当其他线程访问该对象时,可能会遇到未完全初始化的状态,导致不可预料的行为或错误。
将引用指向分配的内存空间的顺序被重排:类似地,将对象引用赋值给变量的操作也可能被重排到对象初始化之前。这意味着一个线程可以获取到非空的对象引用,但实际上该引用可能还没有指向有效的内存空间。在这种情况下,其他线程可能会访问到无效的内存地址,导致错误或异常。
本文作者:YaosGHC
本文链接:https://www.cnblogs.com/yaocy/p/16263861.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步