Fork me on GitHub

【趣味设计模式系列】之【单例模式】

 


1. 简介

单例模式(Singleton):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

2. 图解

类图如下:

3. 案例实现

单例特点:

  • 外部类不能随便对单例类创建,故单例的构造方法必须为private,在类的内部自行实例化;
  • 提供一个public方法入口,作为唯一调用单例类的途径得到实例。

3.1 饿汉式

package com.wzj.singleton; /** * @Author: wzj * @Date: 2020/2/13 12:41 * @Desc: 饿汉式 * 类加载到内存后,就实例化一个单例,JVM保证线程安全 * 唯一缺点:不管用到与否,类装载时就完成实例化 */ public class HungrySingleton { private static final HungrySingleton HUNGRY_SINGLETON = new HungrySingleton(); /** * 私有构造方法,只有本类才能调用 */ private HungrySingleton() { } public static HungrySingleton getInstance() { return HUNGRY_SINGLETON; } public static void main(String[] args) { HungrySingleton h1 = HungrySingleton.getInstance(); HungrySingleton h2 = HungrySingleton.getInstance(); System.out.println(h1 == h2); } }

执行结果:

true
  • 分析:类加载到内存,就实例化一个单例,通过final的静态变量保证唯一实例。
  • 优点:线程安全。
  • 缺点:不管是否用到,类装载时就完成实例化。

3.2 懒汉式

package com.wzj.singleton; /** * @Author: wzj * @Date: 2020/2/13 12:50 * @Desc: 懒汉式-线程不安全 */ public class LazySingleton { private static LazySingleton LAZY_SINGLETON; private LazySingleton() { } public static LazySingleton getInstance() { if (null == LAZY_SINGLETON) { //睡眠1毫秒,在new对象前,增加被其他线程打断的机会,保证能被多个线程执行 try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } LAZY_SINGLETON = new LazySingleton(); } return LAZY_SINGLETON; } public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(() -> System.out.println(LAZY_SINGLETON.getInstance().hashCode()) ).start(); } } }

执行结果

1338303845 1276043710 874014640 2116194883 835777993 527405373 38860249 772510047 484927678 1375039115 . . .
  • 分析:懒加载在调用getInstace方法的时候创建实例,通过100个线程测试发现其hashcode并不相等,并不是单例。
  • 优点:改善了饿汉式中实例不用也加载的弊端。
  • 缺点:引入了新的问题,线程不安全,并不能保证单例

3.3 synchronized修饰方法单例

package com.wzj.singleton; /** * @Author: wzj * @Date: 2020/2/13 13:50 * @Desc: 懒汉式-线程安全 * 通过synchronized解决,但获取锁带来性能开销,效率下降 */ public class ThreadSafeSingleton { private static ThreadSafeSingleton LAZY_SINGLETON; private ThreadSafeSingleton() { } public static synchronized ThreadSafeSingleton getInstance() { if (null == LAZY_SINGLETON) { //睡眠1毫秒,在new对象前,增加被其他线程打断的机会,保证能被多个线程执行 try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } LAZY_SINGLETON = new ThreadSafeSingleton(); } return LAZY_SINGLETON; } public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(() -> System.out.println(LAZY_SINGLETON.getInstance().hashCode()) ).start(); } } }

执行结果

2003664945 2003664945 2003664945 2003664945 2003664945 2003664945 2003664945 2003664945 . . .
  • 分析:通过synchronized关键字保证线程安全。
  • 优点:线程安全,保证单例。
  • 缺点:方法上加锁导致性能开销。

3.4 synchronized修饰代码块的单例

package com.wzj.singleton; /** * @Author: wzj * @Date: 2020/2/13 13:50 * @Desc: 懒汉式-线程不安全 * 试图通过减小同步代码块的方式提高效率,带来了线程不安全 */ public class ThreadUnSafeSingleton { private static ThreadUnSafeSingleton LAZY_SINGLETON; private ThreadUnSafeSingleton() { } public static ThreadUnSafeSingleton getInstance() { if (null == LAZY_SINGLETON) { //试图通过减小同步代码块的方式提高效率,带来了线程不安全 synchronized (ThreadUnSafeSingleton.class) { //睡眠1毫秒,在new对象前,增加被其他线程打断的机会,保证能被多个线程执行 try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } LAZY_SINGLETON = new ThreadUnSafeSingleton(); } } return LAZY_SINGLETON; } public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(() -> System.out.println(LAZY_SINGLETON.getInstance().hashCode()) ).start(); } } }

结果:

1276043710 1276043710 1276043710 1276043710 1276043710 1276043710 2116194883 2116194883 2116194883 2116194883 . . .
  • 分析:试图通过减小同步代码块的方式提高效率,带来了线程不安全。
  • 优点:减小了加锁的范围,提高了性能。
  • 缺点:线程不安全,结果显示不能保证单例。

3.5 双重检测安全单例

package com.wzj.singleton; /** * @Author: wzj * @Date: 2020/2/13 13:50 * @Desc: 双重检测下线程安全 * */ public class DoubleCheckedThreadSafeSingleton { private volatile static DoubleCheckedThreadSafeSingleton LAZY_SINGLETON; private DoubleCheckedThreadSafeSingleton() { } public static DoubleCheckedThreadSafeSingleton getInstance() { if (null == LAZY_SINGLETON) { //试图通过减小同步代码块的方式提高效率,带来了线程不安全 synchronized (DoubleCheckedThreadSafeSingleton.class) { if (null == LAZY_SINGLETON) { //睡眠1毫秒,在new对象前,增加被其他线程打断的机会,保证能被多个线程执行 try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } LAZY_SINGLETON = new DoubleCheckedThreadSafeSingleton(); } } } return LAZY_SINGLETON; } public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(() -> System.out.println(LAZY_SINGLETON.getInstance().hashCode()) ).start(); } } }

执行结果

527405373 527405373 527405373 527405373 527405373 527405373 527405373 527405373 . . .
  • 分析:第一个判空语句if (null == LAZY_SINGLETON),用来检测如果内存中有单例生成以后,永不进入下面的代码,直接走return语句返回已有的单例,第二个判空语句,保证当前线程拿到锁的前后,内存中都没有单例,才执行创建单例操作,防止中途被其他线程创建单例,进而重复创建;volatile关键字保证在执行语句LAZY_SINGLETON = new DoubleCheckedThreadSafeSingleton() 时,可以分解为如下的3行伪代码。
memory = allocate();  // 1:分配对象的内存空间 ctorInstance(memory); // 2:初始化对象 instance = memory;  // 3:设置instance指向刚分配的内存地址

上面3行伪代码中的2和3之间,可能会被重排序(在一些JIT编译器上,这种重排序是真实发生的)。2和3之间重排序之后的执行时序如下。

memory = allocate();  // 1:分配对象的内存空间 instance = memory;  // 3:设置instance指向刚分配的内存地址 // 注意,此时对象还没有被初始化! ctorInstance(memory); // 2:初始化对象

上面的代码在编译器运行时,可能会出现重排序 从1-2-3 排序为1-3-2,如果发生重排序,另一个并发执行的线程B就有可能在判断instance不为null。线程B接下来将访问instance所引用的对象,但此时这个对象可能还没有被A线程初始化!

  • 优点:保证单例与线程安全。
  • 缺点:增加了代码的复杂度。

3.6 内部静态类单例

package com.wzj.singleton; /** * @Author: wzj * @Date: 2020/2/13 15:45 * @Desc: 静态内部类方式 * JVM保证单例 * 加载外部类时不会加载内部类,这样可以实现懒加载 */ public class InnerStaticClassSingleton { private InnerStaticClassSingleton() { } //内部静态类 private static class InnerStaticClassSingletonHolder { private static final InnerStaticClassSingleton INSTANCE = new InnerStaticClassSingleton(); } public static InnerStaticClassSingleton getInstance() { return InnerStaticClassSingletonHolder.INSTANCE; } public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(() -> System.out.println(InnerStaticClassSingleton.getInstance().hashCode()) ).start(); } } }

执行结果:

772510047 772510047 772510047 772510047 772510047 772510047 772510047 772510047 772510047 772510047 . . .
  • 分析:因为虚拟机加载类的时候只加载一次,并且加载外部类时不会加载内部类,只有调用getInstance方法的时候,内部类才被加载,所以内部类的静态变量也只加载一次,JVM保证了单例与线程安全。
  • 优点:线程安全。
  • 缺点:无。

3.7 枚举单例

package com.wzj.singleton; /** * @Author: wzj * @Date: 2020/2/13 16:05 * @Desc: 枚举实现,既可以保证线程安全,还可以防止反序列化。 */ public enum EnumSingleton { INSTANCE; public static void main(String[] args) { for(int i=0; i<100; i++) { new Thread(()->{ System.out.println(EnumSingleton.INSTANCE.hashCode()); }).start(); } } }
  • 优点:线程安全,同时保证不被反序列化,因为枚举类型没有构造方法,不能反序列化后创建对象。
  • 缺点:写法优点怪异。

4. 框架源码分析

以下源码分析基于Spring5.0.6 RELEASE。DefaultSingletonBeanRegistry.class部分源码如下.

@Nullable public Object getSingleton(String beanName) { return this.getSingleton(beanName, true); } @Nullable protected Object getSingleton(String beanName, boolean allowEarlyReference) { // 从单例缓存容器中加载 bean Object singletonObject = this.singletonObjects.get(beanName); // 单例缓存容器中的 bean 为空,且当前 bean 正在创建 if (singletonObject == null && this.isSingletonCurrentlyInCreation(beanName)) { // 加锁 synchronized(this.singletonObjects) { // 从 earlySingletonObjects 容器中获取 singletonObject = this.earlySingletonObjects.get(beanName); // earlySingletonObjects容器中没有,且允许提前创建 if (singletonObject == null && allowEarlyReference) { // 从 singletonFactories 中获取对应的 ObjectFactory ObjectFactory<?> singletonFactory = (ObjectFactory)this.singletonFactories.get(beanName); // ObjectFactory 不为空,则创建 bean if (singletonFactory != null) { // 获取 bean singletonObject = singletonFactory.getObject(); // 添加 bean 到 earlySingletonObjects 中 this.earlySingletonObjects.put(beanName, singletonObject); // 从 singletonFactories 中移除对应的 ObjectFactory this.singletonFactories.remove(beanName); } } } } return singletonObject; }
  • 第一步,从singletonObjects 中,获取 Bean 对象。
  • 第二步,若为空且当前bean正在创建中,则从earlySingletonObjects中获取Bean对象。
  • 第三步,若为空且允许提前创建,则从singletonFactories中获取相应的ObjectFactory对象。若不为空,则调用其ObjectFactorygetObject(String name)方法,创建Bean对象,然后将其加入到earlySingletonObjects ,然后从singletonFactories删除。
    由此可见,Spring在创建单例bean的时候,采用的是双重检测加锁机制创建bean的。

5. 单例与静态方法的比较

  • 单例支持延迟加载,静态类第一次加载就初始化;
  • 单例常驻内存,除非JVM退出,静态方法中的对象,会随着静态方法执行完被释放,gc回收。

6. 应用场景

  • 数据库连接池,因为频繁建立或者关闭数据库连接,损耗性能非常大,因为何用单例模式来维护,就可以大大降低这种损耗;
  • 线程池,因为线程是一种稀缺资源,频繁创建线程,会导系统开销增大,线程之间的频繁切换也导致性能下降,由统一的线程池管理线程;
  • 开发中常用的配置工具类,因为配置类是共享的资源;
  • 日志应用,因为日志属于工享文件,一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加;
  • Windows的任务管理器,多次打开只会弹出一个对话框,确保系统由一个任务管理器管理;
  • 网站的计数器,如果多个,计数难以同步;
    综上,单例应用在共享资源上,要么方便管理,要么节约性能,避免不必要的性能开销。

7. 总结

单例的具体写法,需要结合场景与业务要求,确认是否支持线程安全,是否支持延迟加载,单例比较简单的写法是饿汉式,唯一的不足是不支持懒加载,还有静态内部类;比较完美的写法是双重检测加锁,虽然写法复杂,但支持延迟加载,线程安全,也是Spring源码使用的方式。


__EOF__

本文作者小猪爸爸
本文链接https://www.cnblogs.com/father-of-little-pig/p/12304306.html
关于博主:不要为了技术而技术,总结分享技术,感恩点滴生活!
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   小猪爸爸  阅读(464)  评论(0编辑  收藏  举报
编辑推荐:
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
点击右上角即可分享
微信分享提示