跳到主要内容

单例模式

单例模式的定义

单例设计模式(Singleton Design Pattern)是一种创建型设计模式,它确保一个类只允许创建一个对象(或者实例),并提供一个全局访问点来访问这个唯一实例。

  1. 保证一个类只有一个实例。
  2. 为该实例提供一个全局访问点。

为什么使用单例模式?

单例模式主要用于以下两种场景:

1. 处理资源访问冲突

在多线程环境下,当多个线程同时访问同一个共享资源时,可能会发生资源访问冲突。使用单例模式可以确保对共享资源的访问是线程安全的。

例如,一个日志记录器(Logger)类,如果每个线程都创建自己的实例,可能会导致日志文件写入冲突:

public class Logger {
private FileWriter writer;
private static final Logger instance = new Logger();

private Logger() {
try {
File file = new File("log.txt");
writer = new FileWriter(file, true); // true表示追加写入
} catch (IOException e) {
throw new RuntimeException(e);
}
}

public static Logger getInstance() {
return instance;
}

public void log(String message) {
try {
writer.write(message + "\n");
writer.flush();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

2. 表示全局唯一实例

某些类在系统中天然就应该只有一个实例,例如:

  • 系统配置类:系统中只需要一份配置信息
  • 线程池:系统级的线程池应该只有一个
  • 计数器:用于生成唯一ID的计数器

例如,一个全局唯一的ID生成器:

public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private static final IdGenerator instance = new IdGenerator();

private IdGenerator() {}

public static IdGenerator getInstance() {
return instance;
}

public long getId() {
return id.incrementAndGet();
}
}
springboot 示例
//

初始化阶段:
Spring容器创建 IdWorkerUtils Bean
执行 init() 方法获取机器ID并保存实例到静态变量
ID生成阶段:
外部调用 IdWorkerUtils.generateId()
静态方法通过 idWorkerUtils 实例调用 nextId()
nextId() 根据Snowflake算法生成唯一ID
//

package com.e6yun.project.tms.common.util;

import com.e6yun.project.common.redis.E6RedisService;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class IdWorkerUtils {
@Resource
private E6RedisService redisService;
@Value("${spring.application.name:default}")
private String appName;
private static final Logger logger = LoggerFactory.getLogger(IdWorkerUtils.class);
private static final long twepoch = 1489111610226L;
private static final long workerIdBits = 10L;
private final long maxWorkerId = 1023L;
private static final long sequenceBits = 12L;
private static final long workerIdShift = 12L;
private static final long timestampLeftShift = 22L;
private static final long sequenceMask = 4095L;
private static long lastTimestamp = -1L;
private long sequence = 0L;
private long workerId;
private static IdWorkerUtils idWorkerUtils;

public IdWorkerUtils() {
}

@PostConstruct
void init() {
this.workerId = this.getMachineNum();
if (this.workerId >= 0L && this.workerId <= 1023L) {
logger.info("idWorkerUtils, workerId origin:{}", this.workerId);
idWorkerUtils = this;
} else {
throw new RuntimeException(String.format("IdWorkerUtils worker Id can't be greater than %d or less than 0", 1023L));
}
}

private long getMachineNum() {
try {
long increment = this.redisService.getStringRedisTemplate().opsForValue().increment(this.genFeatureIdKey(), 1L);
return increment & 1023L;
} catch (Exception var3) {
Exception e = var3;
throw new RuntimeException("IdWorkerUtils workerId increment error", e);
}
}

String genFeatureIdKey() {
return "snowflake-featureId".concat("-").concat(this.appName);
}

private synchronized long nextId() {
long timestamp = this.timeGen();
if (timestamp < lastTimestamp) {
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
} else {
if (lastTimestamp == timestamp) {
this.sequence = this.sequence + 1L & 4095L;
if (this.sequence == 0L) {
timestamp = this.tilNextMillis(lastTimestamp);
}
} else {
this.sequence = 0L;
}

lastTimestamp = timestamp;
return timestamp - 1489111610226L << 22 | this.workerId << 12 | this.sequence;
}
}

protected long tilNextMillis(long lastTimestamp) {
long timestamp;
for(timestamp = this.timeGen(); timestamp <= lastTimestamp; timestamp = this.timeGen()) {
}

return timestamp;
}

protected long timeGen() {
return System.currentTimeMillis();
}

public static synchronized String generateId() {
return String.valueOf(idWorkerUtils.nextId());
}
}

单例模式的实现方式

1. 饿汉式

特点:

  • 在类加载时就完成了实例化
  • 线程安全
  • 不支持延迟加载,获取实例对象的速度快,但是如果对象比较大,而且一直没有使用的,就会造成内存的浪费 。
public class Singleton {
//2. 在本类中创建私有的静态全局对象, static 对象会随着类的加载而加载,且只有一次,由 JVM 保证
private static final Singleton instance = new Singleton();
//1. 私有构造方法
private Singleton() {}
//3. 提供一个全局访问点,供外部获取单例对象
public static Singleton getInstance() {
return instance;
}
}

2. 懒汉式

特点:

  • 支持延迟加载
  • 线程安全但并发性能差,通过添加 synchronized 关键字实现
  • 加锁导致性能开销大,获取实例函数的并发度很低
public class Singleton {
private static Singleton instance;
//1. 私有构造方法
private Singleton() {}
//2. 提供一个全局访问点,供外部获取单例对象。通过判断是否被初始化,来选择是否创建对象。
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

3. 双重检测

特点:

  • 支持延迟加载
  • 线程安全且并发性能好
  • 实现相对复杂
public class Singleton {
//2. 创建私有静态的全局对象
private static volatile Singleton instance;
//1. 私有构造方法
private Singleton() {}
//3. 获取单例对象的静态方法
public static Singleton getInstance() {
// 第一次判断,如果 instance 不为 null,不进入抢锁阶段,直接返回实例
if (instance == null) {
synchronized (Singleton.class) {
// 第二次判断,抢到锁之后,再次进行判断是否为 null 。
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

为什么需要 volatile?

在双重检测模式中,使用 volatile 关键字是为了防止指令重排序导致的问题。创建一个对象实际上会执行以下三个步骤:

  1. 分配内存空间
  2. 初始化对象
  3. 将引用指向分配的内存空间

由于 JVM 的指令重排优化,这三个步骤的实际执行顺序可能变成 1->3->2。如果是这样,在多线程环境下可能出现以下问题:

// 线程A执行
memory = allocate(); // 1. 分配内存
instance = memory; // 3. 设置instance指向内存空间
instance.init(); // 2. 初始化对象

// 线程B执行
if (instance != null) { // 看到instance不为null,但实际对象还未初始化完成
instance.doSomething(); // 访问未初始化完成的对象,可能导致异常
}

使用 volatile(保证对象可见性,屏蔽指令重排序) 关键字可以禁止这种重排序,它保证:

  • 写操作不会和之前的操作重排序
  • 读操作不会和之后的操作重排序
  • 对象的发布对所有线程立即可见

4. 静态内部类

特点:

  • 支持延迟加载
  • 线程安全
  • 实现简单

根据静态内部类的特性,同时解决了延时加载、线程安全的问题,并且代码更加简洁。

public class Singleton {
//1. 私有构造方法
private Singleton() {}
//2. 创建静态内部类
private static class SingletonHolder {
// 在静态内部类中创建单例,在加载内部类的时候才会创建单例对象,只会加载一次,由 JVM 保证线程安全。
private static final Singleton instance = new Singleton();
}

public static Singleton getInstance() {
return SingletonHolder.instance; // 首次访问时加载内部类,创建单例对象
}
}

5. 枚举

特点:

  • 实现最简单
  • 线程安全
  • 防止反序列化创建新实例
public enum Singleton {
INSTANCE;

public void doSomething() {
// 业务方法
}
}
提示
  1. 如果不需要延迟加载,优先使用饿汉式
  2. 如果需要延迟加载,推荐使用静态内部类方式
  3. 如果涉及序列化,推荐使用枚举方式
  4. 双重检测虽然是一个好的选择,但要注意 volatile 关键字的正确使用

单例模式的问题

虽然单例模式使用广泛,但它也存在一些严重的问题,有些人甚至认为它是一种反模式(anti-pattern)。以下是主要问题:

1. 对 OOP 特性支持不友好

单例模式违背了面向对象的基本原则:

  1. 违背基于接口而非实现的设计原则
public class Order {
public void create(...) {
//...
long id = IdGenerator.getInstance().getId();
//...
}
}

// 如果要针对不同业务使用不同ID生成器,需要大量修改代码
public class Order {
public void create(...) {
//...
long id = OrderIdGenerator.getInstance().getId();
//...
}
}
  1. 不利于继承和多态:一旦将类设计为单例,就很难利用继承和多态来扩展其功能。

2. 隐藏类之间的依赖关系

  • 单例模式通过静态方法调用,使得类之间的依赖关系不够明显
  • 代码可读性差,需要仔细阅读代码才能发现类之间的依赖
  • 不利于代码维护和重构

3. 影响代码扩展性

以数据库连接池为例:

// 最初设计:单例的数据库连接池
public class DBConnectionPool {
private static final DBConnectionPool instance = new DBConnectionPool();
private DBConnectionPool() {}

public static DBConnectionPool getInstance() {
return instance;
}
}

// 需求变更:需要两个连接池分别处理快慢SQL
// 但单例模式使得无法创建多个实例,需要重构整个类设计

所以,数据库连接池、线程池这类的资源池,最好还是不要设计成单例类。实际上,一些开源的数据库连接池、线程池也确实没有设计成单例类。

4. 不利于单元测试

  1. 难以 Mock
public class UserService {
public void createUser(...) {
// 直接使用单例,难以在测试时替换为mock对象
long id = IdGenerator.getInstance().getId();
}
}
  1. 全局状态问题:如果单例维护了状态,会影响不同测试用例之间的独立性

5. 构造函数参数传递问题

单例模式难以优雅地支持带参数的构造函数,虽然有以下几种解决方案,但都不够完美:

  1. 通过 init 方法
public class Singleton {
private static Singleton instance = null;
private final int paramA;
private final int paramB;

private Singleton(int paramA, int paramB) {
this.paramA = paramA;
this.paramB = paramB;
}

public synchronized static Singleton init(int paramA, int paramB) {
if (instance != null) {
throw new RuntimeException("Singleton has been created!");
}
instance = new Singleton(paramA, paramB);
return instance;
}
}
  1. 通过配置类
public class Config {
public static final int PARAM_A = 123;
public static final int PARAM_B = 245;
}

public class Singleton {
private static Singleton instance = null;
private final int paramA;
private final int paramB;

private Singleton() {
this.paramA = Config.PARAM_A;
this.paramB = Config.PARAM_B;
}
}

替代方案

1. 依赖注入

将单例对象作为依赖注入到需要使用它的类中:

// 原来的方式
public class UserService {
public void createUser(...) {
long id = IdGenerator.getInstance().getId();
}
}

// 依赖注入方式
public class UserService {
private final IdGenerator idGenerator;

public UserService(IdGenerator idGenerator) {
this.idGenerator = idGenerator;
}

public void createUser(...) {
long id = idGenerator.getId();
}
}

2. 使用 IOC 容器

利用 Spring 等 IOC 容器来管理对象的生命周期和依赖关系:

@Component
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);

public long getId() {
return id.incrementAndGet();
}
}

@Service
public class UserService {
@Autowired
private IdGenerator idGenerator;

public void createUser(...) {
long id = idGenerator.getId();
}
}

3. 工厂模式

使用工厂模式来控制对象的创建:

public class IdGeneratorFactory {
private static final Map<String, IdGenerator> generators = new ConcurrentHashMap<>();

public static IdGenerator getGenerator(String type) {
return generators.computeIfAbsent(type, k -> createGenerator(k));
}

private static IdGenerator createGenerator(String type) {
// 根据类型创建不同的生成器
return new IdGenerator();
}
}

4. 静态方法

对于简单的场景,可以使用静态方法代替单例:

public class IdGenerator {
private static AtomicLong id = new AtomicLong(0);

public static long getId() {
return id.incrementAndGet();
}
}

不过,静态方法这种实现思路,并不能解决之前提到的问题。

单例模式的作用范围

进程唯一

这是最常见的单例实现方式,单例对象在进程内是唯一的:

  • 进程内唯一:同一个进程内只能有一个实例
  • 进程间不唯一:不同进程可以各自创建实例
  • 进程唯一意味着线程内、线程间都唯一
public class ProcessSingleton {
private static ProcessSingleton instance;

private ProcessSingleton() {}

public static synchronized ProcessSingleton getInstance() {
if (instance == null) {
instance = new ProcessSingleton();
}
return instance;
}
}

线程唯一

线程唯一的单例表示:

  • 线程内唯一:同一个线程内只能有一个实例
  • 线程间可以不唯一:不同线程可以各自创建实例

实现方式有两种:

  1. 使用HashMap存储
public class ThreadSingleton {
private static final ConcurrentHashMap<Long, ThreadSingleton> instances
= new ConcurrentHashMap<>();

private ThreadSingleton() {}

public static ThreadSingleton getInstance() {
Long currentThreadId = Thread.currentThread().getId();
instances.putIfAbsent(currentThreadId, new ThreadSingleton());
return instances.get(currentThreadId);
}
}
  1. 使用ThreadLocal工具类
public class ThreadLocalSingleton {
private static final ThreadLocal<ThreadLocalSingleton> instance =
new ThreadLocal<ThreadLocalSingleton>() {
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};

private ThreadLocalSingleton() {}

public static ThreadLocalSingleton getInstance() {
return instance.get();
}
}

集群唯一

集群唯一是最严格的单例实现:

  • 进程内唯一
  • 进程间也唯一
  • 整个集群环境中只有一个实例

实现思路:

  1. 将单例对象序列化到外部共享存储(如文件系统)
  2. 使用分布式锁保证任意时刻只有一个进程可以访问对象
  3. 使用完后需要释放锁并删除内存中的对象

示例实现:

public class ClusterSingleton {
private AtomicLong id = new AtomicLong(0);
private static ClusterSingleton instance;
private static SharedObjectStorage storage = new FileSharedObjectStorage();
private static DistributedLock lock = new DistributedLock();

private ClusterSingleton() {}

public static synchronized ClusterSingleton getInstance() {
if (instance == null) {
lock.lock();
instance = storage.load(ClusterSingleton.class);
}
return instance;
}

public synchronized void freeInstance() {
storage.save(this, ClusterSingleton.class);
instance = null; // 释放对象
lock.unlock();
}

public long getId() {
return id.incrementAndGet();
}
}
序列化破坏单例问题解决
package com.e6yun.project;

import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.nio.file.Files;
import java.nio.file.Paths;

public class TestSingletonSerializable {

public static void main(String[] args) throws IOException, ClassNotFoundException {
// 序列化对象输出流
ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("tempFile.obj")));
oos.writeObject(Singleton.getInstance());

// 序列化对象输入流
File file = new File("tempFile.obj");
ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(file.toPath()));
/**
*
*/
Singleton singleton = (Singleton) ois.readObject();

System.out.println( singleton); // com.e6yun.project.Singleton@27d6c5e0
System.out.println(Singleton.getInstance()); //com.e6yun.project.Singleton@330bedb4
System.out.println(Singleton.getInstance()==singleton); // false
}
}

/**
* 单例类实现序列化接口
*/
class Singleton implements Serializable {

private static volatile Singleton singleton;

private Singleton() {
}

public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}

/**
* 只需要在单例类中定义 readResolve 方法,就可以解决序列化都单例的破坏
* @return
*/
private Object readResolve(){
return singleton;
}
}

多例模式

与单例模式相对的是多例模式,允许创建有限个数的实例。实现方式有两种:

  1. 限制实例数量
public class LimitedMultiton {
private static final int MAX_NUM = 3;
private static final Map<Integer, LimitedMultiton> instances = new HashMap<>();

static {
for(int i = 0; i < MAX_NUM; i++) {
instances.put(i, new LimitedMultiton());
}
}

private LimitedMultiton() {}

public static LimitedMultiton getInstance(int index) {
if(index < 0 || index >= MAX_NUM) {
throw new IllegalArgumentException();
}
return instances.get(index);
}
}
  1. 基于类型的多例
public class TypeMultiton {
private static final ConcurrentHashMap<String, TypeMultiton> instances
= new ConcurrentHashMap<>();

private TypeMultiton() {}

public static TypeMultiton getInstance(String type) {
instances.putIfAbsent(type, new TypeMultiton());
return instances.get(type);
}
}

注意事项

  1. 构造函数必须是私有的,防止外部直接创建实例
  2. 考虑对象创建时的线程安全问题
  3. 注意是否需要支持延迟加载
  4. 考虑 getInstance() 的性能(是否需要加锁)
  5. 如果单例类可能被序列化,需要特殊处理防止反序列化破坏单例
  6. 在Java中,单例的唯一性实际上是基于类加载器(ClassLoader)的,而不是进程
  7. 集群环境下的单例实现较为复杂,需要考虑:
    • 分布式锁的实现
    • 对象序列化和反序列化
    • 共享存储的可靠性
  8. 多例模式在实际应用中较少使用,但在某些特定场景下很有用
  9. 使用ThreadLocal实现线程唯一单例时要注意内存泄漏问题
  10. 需要防止反射攻击,防止通过反射创建多个实例