在上篇 《单例模式(上)》一文中介绍了单例定义、使用场景、实现方式以及不足,本篇继续整理针对不足的解决方案以及唯一性的相关讨论与实现等。
为了保证全局唯一,除了使用单例, 可以用静态方法来实现。但静态方法比单例更加不灵活,比如,它无法支持延迟加载。
public class IdGenerator {
private AtomicLong id = new AtomicLong(0);
// 静态方法实现
public static long getId() {
return id.incrementAndGet();
}
}
// 使用示例
long id = IdGenerator.getId();
通过依赖注入,将单例生成的对象,作为参数传递给函数(也可以通过构造函数传递给类的成员变量),可以解决单例隐藏类之间依赖关系的问题。
// 旧使用方式
public demoFuntion() {
//...
long id = IdGenerator.getInstance().getId();
//...
}
// 2. 新的使用方式:依赖注入
public demofunction(IdGenerator idGenerator) {
//...
long id = idGenerator.getId();
//...
}
// 外部调用demofunction()的时候,传入idGeneratorIdGenerator
idGenerator = IdGenerator.getInsance();
demofunction(idGenerator);
从根源上出发,类对象的全局唯一性可以通过多种不同的方式来保证:
单例模式创建的对象是进程唯一的。
实际上,对于 Java 语言来说,单例类对象的唯一性的作用范围并非进程,而是类加载器(Class Loader),原因在于Java的双亲委派模型。
在代码中,通过一个 HashMap 来存储对象,其中 key 是线程 ID,value 是对象。这样就可以做到,不同的线程对应不同的对象,同一个线程只能对应一个对象。
实际上,Java 语言本身提供了 ThreadLocal 工具类,可以更加轻松地实现线程唯一单例。ThreadLocal 底层实现原理也是基于HashMap的方式。
public class IdGenerator6 {
private AtomicLong id = new AtomicLong(0);
private static final ConcurrentHashMap<Long, IdGenerator6> instances
= new ConcurrentHashMap<>();
private IdGenerator6() {}
public static IdGenerator6 getInstance() {
Long currentThreadId = Thread.currentThread().getId();
instances.putIfAbsent(currentThreadId, new IdGenerator6());
return instances.get(currentThreadId);
}
public long getId() {
return id.incrementAndGet();
}
}
集群相当于多个进程构成的一个集合,“集群唯一”就相当于是进程内唯一、进程间也唯一。即,不同的进程间共享同一个对象,不能创建同一个类的多个对象。
/**
* 集群唯一
*
* [设计模式之美](https://windcoder.com/go/JKDesignPattern)中仅提供了伪代码,这里根据伪代码尝试做了一个基于Redis的简单实现,代码本身没做测试,请勿直接粘贴复制到生产环境。
*
* Redisson 操作redis实现单例对象的序列化存储与反序列化读取。
* Redisson 实现Redis分布式锁,用于取出前的加锁,与操作后的释放锁。
*
* 关于为何配置自定义序列化与反序列化的问题,可以参考[redisson如何序列化](https://www.php.cn/redis/436670.html)。
*/
public class IdGenerator7 implements Serializable {
private AtomicLong id = new AtomicLong(0);
private static IdGenerator7 instance;
static RedissonClient redisson = getRedissonClient();
static RLock lock = redisson.getLock("anyLock");
private IdGenerator7() {}
public synchronized static IdGenerator7 getInstance() {
if (instance == null) {
lock.lock();
RBucket<IdGenerator7> bucket = redisson.getBucket("IdGenerator");
//如果key存在,就设置key的值为新值value
//如果key不存在,就设置key的值为value
// https://www.pianshen.com/article/5465125503/
bucket.set(new IdGenerator7());
instance = bucket.get();
}
return instance;
}
public synchronized void freeInstance() {
RBucket<IdGenerator7> bucket = redisson.getBucket("IdGenerator");
bucket.set(new IdGenerator7());
instance = null; //释放对象
lock.unlock();
}
public long getId() {
return id.incrementAndGet();
}
/**
* 读取Redisson关于Redis的相关配置,生成Redisson对象。
*
* 这里仅为了方便实现与展示集群唯一的示例。
* @return
*/
private static RedissonClient getRedissonClient() {
Config config = null;
try {
config = Config.fromYAML(new File("classpath:redisson.yaml"));
} catch (IOException e) {
e.printStackTrace();
}
return Redisson.create(config);
}
}
核心是通过 **一个 Map ** 来存储对象类型和对象之间的对应关系,来控制对象的个数。
“单例”指的是,一个类只能创建一个对象。对应地,“多例”指的就是,一个类可以创建多个对象,但是个数是有限制的,比如只能创建 3 个对象。
public class IdGenerator8 {
private long serverNo;
private String serverAddress;
private static final int SERVER_COUNT = 3;
private static final ConcurrentHashMap<Long, IdGenerator8> instances
= new ConcurrentHashMap<>();
static {
instances.put(1L, new IdGenerator8(1L, "192.134.22.138:8080"));
instances.put(2L, new IdGenerator8(2L, "192.134.22.139:8080"));
instances.put(3L, new IdGenerator8(3L, "192.134.22.140:8080"));
}
private IdGenerator8(long serverNo, String address) {
this.serverNo = serverNo;
this.serverAddress = address;
}
public IdGenerator8 getInstance(long serverNo) {
return instances.get(serverNo);
}
public IdGenerator8 getRandomInstance() {
Random r = new Random();
int no = r.nextInt(SERVER_COUNT)+1;
return instances.get(no);
}
}
“多例”还有一种理解方式:同一类型的只能创建一个对象,不同类型的可以创建多个对象。
这种多例模式的理解方式有点类似工厂模式。它跟工厂模式的不同之处是,多例模式创建的对象都是同一个类的对象,而工厂模式创建的是不同子类的对象。
public class IdGenerator9 {
private static final ConcurrentHashMap<String, IdGenerator9> instances
= new ConcurrentHashMap<>();
private IdGenerator9() {}
public static IdGenerator9 getInstance(String name) {
instances.putIfAbsent(name, new IdGenerator9());
return instances.get(name);
}
public static void main(String[] args) {
IdGenerator9 i1 = IdGenerator9.getInstance("User.class");
IdGenerator9 i2 = IdGenerator9.getInstance("User.class");
IdGenerator9 i3 = IdGenerator9.getInstance("Order.class");
}
}
如果实例占用资源多(比如占用内存多)或初始化耗时长(比如需要加载各种配置文件),提前初始化实例是一种浪费资源的行为。
如果初始化耗时长,那我们最好不要等到真正要用它的时候,才去执行这个耗时长的初始化过程,这会影响到系统的性能(比如,在响应客户端接口请求的时候,做这个初始化操作,会导致此请求的响应时间变长,甚至超时)。采用饿汉式实现方式,将耗时的初始化操作,提前到程序启动的时候完成,这样就能避免在程序运行的时候,再去初始化导致的性能问题。
如果实例占用资源多,按照 fail-fast 的设计原则(有问题及早暴露),那我们也希望在程序启动时就将这个实例初始化好。如果资源不够,就会在程序启动的时候触发报错(比如 Java 中的 PermGen Space OOM),我们可以立即去修复。这样也能避免在程序运行一段时间后,突然因为初始化这个实例占用资源过多,导致系统崩溃,影响系统的可用性。