单例模式之双重检查锁
初始化基本单例类
老规矩。
public class DoubleLockStyle { /** * volatile关键字,使得instance变量在多个线程间可见,禁止指令重排序优化 * volatile是一个轻量级的同步机制,即轻量锁 */ private static volatile DoubleLockStyle instance; /** * 私有化构造方法(防止外部new新的对象) */ private DoubleLockStyle() { } }
不一样的是,我在属性上使用volatile
关键词修饰了。
volatile?
补充知识啦!
在这个代码中,使用了 volatile 关键字来确保 instance 变量的可见性,避免出现空指针异常等问题。
volatile
是一种修饰符,用于修饰变量。- 当一个变量被声明为
volatile
时,线程在访问该变量时会强制从主内存中读取变量的值,而不是从线程的本地缓存中读取。- 使用
volatile
关键字可以保证多线程之间的变量访问具有可见性和有序性。- 在对该变量进行修改时,线程也会将修改后的值强制刷回主内存,而不是仅仅更新线程的本地缓存。
补充:
volatile
的主要作用是保证共享变量的可见性和有序性。共享变量是指在多个线程之间共享的变量,例如单例模式中的 instance
变量。如果不使用volatile
关键字修饰 instance
变量,在多线程环境下可能会出现空指针异常等问题。
这是因为当一个线程修改了 instance
变量的值时,其他线程可能无法立即看到修改后的值,从而出现空指针异常等问题。
使用 volatile
关键字可以解决这个问题,因为它可以保证对共享变量的修改对其他线程是可见的。
除了可见性和有序性之外,volatile 还可以防止指令重排序。指令重排序是指 CPU 为了提高程序执行的效率而对指令执行的顺序进行调整的行为。在单例模式中,如果 instance 变量没有被声明为 volatile,那么在多线程环境下可能会出现单例对象被重复创建的问题。这是因为在多线程环境下,某些线程可能会在 instance 变量被初始化之前就调用
getInstance()
方法,从而导致多次创建单例对象。通过将 instance 变量声明为 volatile,可以保证在创建单例对象之前,instance 变量已经被正确地初始化了。
双重锁
/** * 提供一个静态的公有方法,加入双重检查代码,解决线程安全问题,同时解决懒加载问题 * 即双重检查锁模式 * * @return instance(单例对象) */ public static DoubleLockStyle getInstance() { if (instance == null) { // 同步代码块,线程安全的创建实例 synchronized (DoubleLockStyle.class) { //之所以要再次判断,是因为可能有多个线程同时进入了第一个if判断 if (instance == null) { instance = new DoubleLockStyle(); } } } return instance; }
在获取方法中,使用synchronized
来同步,使它线程安全。
有缺分析
双重锁模式是一种用于延迟初始化的优化模式,在第一次调用时创建单例对象,并在之后的访问中直接返回该对象。它通过使用双重检查锁定(double checked locking)来保证在多线程环境下只有一个线程可以创建单例对象,并且不会加锁影响程序性能。
优点:
- 线程安全:使用双重锁模式可以保证在多线程环境下只有一个线程可以创建单例对象,并且不会加锁影响程序性能。
- 延迟初始化:在第一次调用时创建单例对象,可以避免不必要的资源浪费和内存占用。
- 性能优化:通过使用双重检查锁定,可以避免不必要的锁竞争,从而提高程序性能。
缺点:
- 实现复杂:双重锁模式的实现相对复杂,需要考虑线程安全和性能等因素,容易出现错误。
- 可读性差:由于双重锁模式的实现比较复杂,代码可读性较差,不易于理解和维护。
- 难以调试:由于双重锁模式涉及到多线程并发访问,因此在调试过程中可能会出现一些难以定位和复现的问题。
一个synchronized为何叫双重锁?
在双重锁模式中,确实只有一个 synchronized
关键字,但是这个 synchronized
关键字是在代码中被使用了两次,因此被称为“双重锁”。
具体来说,双重锁模式通常会在 getInstance
方法中使用 synchronized
关键字来保证线程安全,但是这会影响程序的性能,因为每次访问 getInstance
方法都需要获取锁。为了避免这个问题,双重锁模式使用了一个优化技巧,即只有在第一次调用 getInstance
方法时才会获取锁并创建单例对象,以后的调用都直接返回已经创建好的单例对象,不需要再获取锁。
具体实现时,双重锁模式会在第一次调用 getInstance
方法时进行两次检查,分别使用外部的 if
语句和内部的 synchronized
关键字。外部的 if
语句用于判断单例对象是否已经被创建,如果已经被创建则直接返回单例对象,否则进入内部的 synchronized
关键字块,再次检查单例对象是否已经被创建,如果没有被创建则创建单例对象并返回,否则直接返回已经创建好的单例对象。
这样做的好处是,在多线程环境下,只有一个线程可以进入内部的 synchronized
关键字块,从而保证了线程安全,同时避免了每次访问 getInstance
方法都需要获取锁的性能问题。
单例模式之静态内部类
因为已经熟悉了这个设计模式原理,我就直接放代码了。
public class StaticInnerClassStyle { /** * 私有化构造方法(防止外部new新的对象) */ private StaticInnerClassStyle() { } /** * 静态内部类 */ private static class SingletonInstance { // 静态内部类中的静态变量(单例对象) private static final StaticInnerClassStyle INSTANCE = new StaticInnerClassStyle(); } /** * 提供一个静态的公有方法,直接返回SingletonInstance.INSTANCE * * @return instance(单例对象) */ public static StaticInnerClassStyle getInstance() { return SingletonInstance.INSTANCE; } }
优缺分析
优点:
- 线程安全:静态内部类在第一次使用时才会被加载,因此在多线程环境下也可以保证只有一个线程创建单例对象,避免了线程安全问题。
- 延迟加载:静态内部类模式可以实现延迟加载,即只有在第一次调用
getInstance
方法时才会加载内部类并创建单例对象,避免了在程序启动时就创建单例对象的开销。
缺点:
- 需要额外的类:静态内部类模式需要定义一个额外的类来实现单例模式,如果项目中有大量的单例对象,则会增加代码量。
- 无法传递参数:静态内部类模式无法接受参数,因此无法在创建单例对象时传递参数,这可能会对某些场景造成限制。
总的来说,静态内部类模式是一种性能高、线程安全的单例模式实现方式,适用于大部分场景。
如果需要传递参数或者需要频繁创建单例对象,则可能需要考虑其他的实现方式。
它不是static修饰吗?为什么也可以懒加载
懒加载即延时加载 --> 使用时采取创建对象。
在静态内部类模式中,单例对象是在静态内部类中被创建的。静态内部类只有在第一次被使用时才会被加载,因此单例对象也是在第一次使用时被创建的。这样就实现了延迟加载的效果,即在需要时才创建单例对象,避免了在程序启动时就创建单例对象的开销。
此外,静态内部类中的静态变量和静态方法是在类加载时被初始化的,而静态内部类本身是非常轻量级的,加载和初始化的时间和开销都非常小。因此,静态内部类模式既能够实现懒加载,又不会带来太大的性能损失。
总之,它在静态初始化意料之外,我相信也在你意料之外。
单例模式之枚举单例
/** * @author JanYork * @date 2023/3/1 17:54 * @description 设计模式之单例模式(枚举单例) * 优点:避免序列化和反序列化攻击破坏单例,避免反射攻击破坏单例(枚举类型构造函数是私有的),线程安全,延迟加载,效率较高。 * 缺点:代码复杂度较高。 */ public enum EnumerateSingletons { /** * 枚举单例 */ INSTANCE; public void whateverMethod() { // TODO:do something ,在这里实现单例对象的功能 } }
在上述代码中,INSTANCE
是 EnumSingleton
类型的一个枚举常量,表示单例对象的一个实例。由于枚举类型的特性,INSTANCE
会被自动初始化为单例对象的一个实例,并且保证在整个应用程序的生命周期中只有一个实例。
使用枚举单例的方式非常简单,只需要通过 EnumSingleton.INSTANCE
的方式来获取单例对象即可。例如:
EnumerateSingletons singleton = EnumerateSingletons.INSTANCE; singleton.doSomething();
使用枚举单例的好处在于,它是线程安全、序列化安全、反射安全的,而且代码简洁明了,不容易出错。另外,枚举单例还可以通过枚举类型的特性来添加其他方法和属性,非常灵活。
优缺分析
- 线程安全:枚举类型的实例创建是在类加载的时候完成的,因此不会出现多个线程同时访问创建单例实例的问题,保证了线程安全。
- 序列化安全:枚举类型默认实现了序列化,因此可以保证序列化和反序列化过程中单例的一致性。
- 反射安全:由于枚举类型的特殊性,不会被反射机制创建多个实例,因此可以保证反射安全。
- 简洁明了:枚举单例的代码非常简洁,易于理解和维护。
枚举单例的缺点相对来说比较少,但是也存在一些限制:
- 不支持懒加载:枚举类型的实例创建是在类加载的时候完成的,因此无法实现懒加载的效果。
- 无法继承:枚举类型不能被继承,因此无法通过继承来扩展单例类的功能。
- 有些情况下不太方便使用:例如需要传递参数来创建单例对象的场景,使用枚举单例可能不太方便。
总之,枚举单例是一种非常优秀的单例实现方式,它具有线程安全、序列化安全、反射安全等优点,适用于大多数单例场景,但也存在一些限制和局限性。需要根据具体的场景来选择合适的单例实现方式。
这么多方式我该怎么选?
设计模式本就是业务中优化一些设计带来的概念性设计,我们需要结合业务分析:
- 饿汉式:适用于单例对象较小、创建成本低、不需要懒加载的场景。
- 懒汉式:
- 双重锁:适用于多线程环境,对性能要求较高的场景。
- 静态内部类:适用于多线程环境,对性能要求较高的场景。
- 枚举:适用于单例对象创建成本较高,且需要考虑线程安全、序列化安全、反射安全等问题的场景。
如果你的单例对象创建成本低、不需要考虑线程安全、序列化安全、反射安全等问题,建议使用饿汉式实现单例;如果需要考虑线程安全和性能问题,可以选择懒汉式的双重锁或静态内部类实现方式;如果需要考虑单例对象创建成本较高,需要考虑线程安全、序列化安全、反射安全等问题,建议选择枚举单例实现方式。
当然,在实际的开发中,还需要考虑其他一些因素,如单例对象的生命周期、多线程访问情况、性能要求、并发访问压力等等,才能综合选择最合适的单例实现方式。
Java程序员身边的单例模式
来自某AI(敏感词):