type
status
date
slug
summary
tags
category
password

1、单例模式是什么

单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来访问该实例
单例模式的适用场景:
  • 需要频繁创建和获取的对象,例如 Spring 容器的各种 Bean。
  • 创建对象耗时或资源消耗大,例如 Servlet 的 ServletContext
  • 工具类对象,例如 Jackson 的 ObjectMapper
  • 频繁访问数据库或文件的对象,例如 Druid 的 DruidDataSource
  • 需要控制共享资源访问的场景,例如 Spring 容器的 ApplicationContext

2、单例模式的实现

2.1 懒汉式(非线程安全)

这种是最简单的单例写法,特点:
  • 构造方法使用 private关键字,确保外部只能通过 getInstance() 获取单例。
  • 实例对象使用 static 关键字,确保全局只有一个实例。
  • 线程不安全:并发时可能出现多个单例。
其中最大的问题就是线程不安全,后面的多种方案都是为了解决线程不安全的问题。

2.2 饿汉式(线程安全)

相比起非线程安全的懒汉式写法,最大区别就是在类初始化的时候提前创建好实例。
特点:
  • 线程安全:因为提前创建了,所以是天生的线程安全。
  • 性能影响:在类加载的时候就会初始化,这对应用的启动会造成一定程度的影响。

2.3 懒汉式(使用synchronized

相比起非线程安全的懒汉式写法,最大区别就是 getInstance() 方法使用 synchronized 修饰
特点:
  • 线程安全:因为使用了 synchronized 修饰 getIntance() 方法,可以保证获取实例一定是线程安全。
  • 性能较差:每次获取实例之前都需要先获取锁,导致性能较差。

2.4 懒汉式(Double Check)

单例模式最经典的双重检查(Double Check)写法,相比起使用 synchronized 修饰 getInstance() 的懒汉模式,最大区别就是
  • 使用 volatile 修饰实例(为什么要用 volatile 后面会解释)。
  • synchronized 不再用来修饰 getInstance() 方法,而是用来修饰 Singleton 类,并增加一段双重判断的逻辑。
特点:
  • 线程安全。
  • 性能较高:只在第一次创建实例的时候需要获取锁,后续在获取实例的时候可以直接返回,不需要获取锁。

2.5 饿汉式(静态内部类,推荐)

这种方式巧妙地解决了饿汉式(线程安全)的内存浪费问题和 synchronized 的性能问题。推荐使用这种方式。
  • 避免内存浪费:内部类在方法被调用之前才会初始化,所以不会造成内存浪费和影响程序启动速度。
  • 线程安全:内部类里面的实例其实就是饿汉式(线程安全)的写法,所以可以保证线程安全。

2.6 枚举实现(官方推荐)

《Effective Java》推荐枚举的方式,特点:
  • 线程安全:枚举类型默认就是安全的
  • 避免反序列化破坏单例
但是枚举类型会造成更多的内存消耗。枚举会比使用静态变量多消耗两倍的内存,如果是 Android 应用,尽量避免这种方式。

3、单例模式常见问题

3.1 为什么单例模式中的Double Check要加volatile

volatile 的作用是禁止指令重排序。在 java 里面 new 一个对象在 JVM 层面并不是一个原子操作,大概分为以下三条指令:
  1. 申请一块内存,此时成员变量赋默认值。
  1. 调用类的构造方法,给成员变量赋初始值。
  1. 将这块内存区域赋值给对应的变量。
JIT 编译器在进行代码优化的时候,有可能会进行指令重排序,把第2,3步骤调换。从 JVM 的角度来说这种调换并不会影响最终实例的创建,但是在这段 Double Check 代码里面,在高并发的情况下有可能会发生问题:
  1. 线程 A 在调用new Singleton()的时候,刚执行第二步(把内存区域赋值给INSTANCE 变量,此时内存里面的成员变量都是默认值),还没到第三步(调用类的构造方法,给成员变量赋初始值)的时候,线程 B 也调用了 getInstance 方法。
  1. 线程 B 判断INSTANCE == null,由于此时 INSTANCE 不为空,所以直接返回了INSTANCE 对象,但是此时 INSTANCE 才仅是半初始化状态(成员变量都是默认值),线程 B 拿到的其实是一个不完整的 INSTANCE 对象,这种情况下是会出问题的。
使用 volatile 来修饰 INSTANCE 对象 ,就可以避免指令重排序的问题。但是在实际的生产中,上述的情况发生的其实很少,只有在极高的并发量的时候才会偶尔出现。如果程序并发量不高,不使用 volatile 关键字一般也不会出现问题。
设计模式系列:委派模式Mysql集群篇:Binlog解析
Loading...