单例模式
Singleton Design Pattern, 一个类只允许创建一个对象(或者实例)
- 可以用来解决资源访问冲突的问题,只对外提供一个操作句柄
- 表示全局唯一类:比如系统配置信息类、
实现
饿汉式
在类加载的期间,就已经将 instance 静态实例初始化好了,所以,instance 实例的创建是线程安全的。不过,这样的实现方式不支持延迟加载实例
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();
}
}
懒汉式
支持延迟加载。为了保证线程安全,这种实现方式会导致频繁加锁、释放锁,以及并发度低等问题,频繁的调用会产生性能瓶颈。
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private static IdGenerator instance;
private IdGenerator() {}
// 获取实例静态方法,每次调用都加锁,保证并发安全。方法第一次被调用时才执行创建对象逻辑
public static synchronized IdGenerator getInstance() {
if (instance == null) {
instance = new IdGenerator();
}
return instance;
}
public long getId() {
return id.incrementAndGet();
}
}
双重检测
既支持延迟加载、又支持高并发的单例实现方式。只要 instance 被创建之后,再调用 getInstance() 函数都不会进入到加锁逻辑中。所以,这种实现方式解决了懒汉式并发度低的问题。
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private static IdGenerator instance;
private IdGenerator() {}
public static IdGenerator getInstance() {
// 第一重检查:是否已经创建了实例,如果没有才会执行加锁逻辑
if (instance == null) {
synchronized(IdGenerator.class) { // 此处为类级别的锁
// 第二次检查:是否已经创建了实例,如果还没有才会执行创建对象逻辑
if (instance == null) {
instance = new IdGenerator();
}
}
}
return instance;
}
public long getId() {
return id.incrementAndGet();
}
}
jdk1.5 (低版本jdk)指令重拍问题,高并发创建实例时,如果 instance 被一个线程创建,并赋值给了 instance 成员,但是测试还没有初始化对象,其他线程使用会出问题
- 本质:对象创建和初始化不是原子操作
- 解决:加 volatile 修饰
高版本 JDK 内部实现中解决了这个问题,将创建对象和初始化设计成一个原则操作
静态内部类实现单例
利用 Java 的静态内部类来实现单例。这种实现方式,既支持延迟加载,也支持高并发,实现起来也比双重检测简单。枚举。instance 的唯一性、创建过程的线程安全性,都由 JVM 来保证(本质还是类加载时创建)。
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
private IdGenerator() {}
// 获取实例静态方法
public static IdGenerator getInstance() {
// 第一次执行时 JVM 会加载SingletonHolder类
return SingletonHolder.instance;
}
// 静态内部类。
private static class SingletonHolder{
// 静态成员,SingletonHolder 类加载时创建 IdGenerator 对象实例
private static final IdGenerator instance = new IdGenerator();
}
public long getId() {
return id.incrementAndGet();
}
}
枚举实现单例
最简单的实现方式,基于枚举类型的本身的特性实现。简单安全
public enum IdGenerator {
INSTANCE;
private AtomicLong id = new AtomicLong(0);
public long getId() {
return id.incrementAndGet();
}
}
IdGenerator.INSTANCE.getId(); // 获得id
枚举的构造方法私有,同时每个枚举实例都是static final类型的,也就表明只能被实例化一次。在加载枚举类 IdGenerator 时,
单例模式存在的问题
- 单例对 OOP 特性的支持不友好
- 单例会隐藏类之间的依赖关系
- 单例对代码的扩展性不友好
- 单例对代码的可测试性不友好
- 单例不支持有参数的构造函数 (可以利用配置类、配置文件)
如果单例类并没有后续扩展的需求,并且不依赖外部系统,那设计成单例类就没有太大问题。
单例类的替代方案:通过工厂模式、IOC 容器保证;通过开发者编码保证…
单例的范围
- 进程内单例
同一个进程中只有该类的一个对象
- 线程内单例
实现:利用 HashMap 来存储对象,其中 key 是线程 ID,value 是对象。Java 语言本身提供了 ThreadLocal 并发工具类,可以更加轻松地实现线程唯一单例。
- 集群环境下的单例,跨进程单例
实现:单例对象序列化并存储到外部共享存储区。进程使用对象时需要读取并反序列化成对象,用完后再将对象序列化存回共享存储区。为了保证任何时刻在进程间都只有一份对象存在,一个进程在获取到对象之后,需要对对象加锁,避免其他进程再将其获取。在进程使用完这个对象之后,需要显式地将对象从内存中删除,并且释放对对象的加锁。
多例模式
一个类可以创建多个对象,但是个数是有限制的。同一类型的只能创建一个对象,不同类型的可以创建多个对象。(这里的类型是一个广义含义,比如按照名称分类,不同名称是不同的类型)
public class Logger {
private static final ConcurrentHashMap<String, Logger> instances
= new ConcurrentHashMap<>();
private Logger() {}
public static Logger getInstance(String loggerName) {
instances.putIfAbsent(loggerName, new Logger());
return instances.get(loggerName);
}
public void log() {
//...
}
}
//l1==l2, l1!=l3
Logger l1 = Logger.getInstance("User.class");
Logger l2 = Logger.getInstance("User.class");
Logger l3 = Logger.getInstance("Order.class");
利用枚举实现多例模式更简单。
多例模式类似工厂模式,区别在于:多例模式创建的时同一个类的对象,工厂模式一般创建的时不同子类的对象。