距离《单例模式上篇》写出去已经很久了,竟然久久没有更新下篇,这是庸俗人的普遍表现,只有开始,没有继续,也没有结束;干什么事都没有恒心,只有三天热度。要坚持啊!
《单例模式上篇》描述了单例的几个核心问题:
为什么要有单例?
正确单例应该怎么写?
典型的单例模式写法?
接下来,我们来进阶一下,拓展一下单例的高级用法,所谓开拓思路,不亦乐乎嘛!
- 单例模式的唯一性如何理解?
- 线程唯一的单例怎么实现?
- 如何实现集群模式下的单例?
- 怎么实现“多例”模式?
看着是不是有些头大,不要急,听我慢慢道来。
单例模式的唯一性怎么理解
我们先来看下单例的定义:“一个类只允许创建唯一一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。”
一个类只允许创建唯一一个对象,这个唯一性的范围是什么?我们都知道程序运行的最小单位是线程,平时编写的一个java程序,最终打包成一个jar包,通过jvm解释执行,对于计算机系统来说,一个程序的运行单位就是一个进程。进程内可包含多个线程,同一个进程内的线程共享进程的内存空间;不同进程间的内存互相隔离。所以这里说的单例模式的唯一性,指的是同一个进程内,只能创建一个单例对象。
能实现线程唯一的单例吗?
既然平时我们编写的程序都是进程唯一的单例,那么问题来了?可以编写出线程唯一的单例吗?什么叫线程唯一的单例呢?
比如说一个单例类SingleTon,在同一个进程内,有多个线程,假设分别是线程A、线程B、线程C…,线程A内只能创建一个SingleTon的对象,线程B内只能创建另一个SingleTon的对象…各线程间的单例对象各不相同。
你可能会问,这有什么意义呢?还记得ThreadLocal吗?这里就有些类似隔离线程间的对象。废话少说,放码过来吧!
public class SingleTon {
private static final Map<Long, SingleTon> singleTonMap = new ConcurrentHashMap<>();
private SingleTon() {}
public static SingleTon getInstance() {
long threadId = Thread.currentThread().getId();
singleTonMap.putIfAbsent(threadId, new SingleTon());
return singleTonMap.get(threadId);
}
public void method() {}
}
集群下唯一单例
首先什么叫集群下唯一的单例呢?
对比kafka、redis集群,我们知道一个集群可能包含多个机器,那肯定也是包含多个进程(多个线程)的了。连机器都跨越了,进程肯定都不一样了。这实现起来好像有点难度了。因为我们不仅要保证线程间唯一,还要保证单例对象在进程间唯一。
要保证集群内各进程访问单例的唯一性,首先需要保证同一时刻只有进程或线程可以获取到单例对象,这个可以通过redis或zookeeper来实现分布式锁。那如何保证单例的唯一性呢?有可能是多台机器执行同样的程序,那单纯的SingleTon单例已经无法保证唯一了,既然组成了一个集群,那么必然有集群的共享存储,如果我们将单例对象存储到集群的共享存储,具体来说,进程使用单例对象时,需要将外部存储区的实例对象读取到内存,反序列化成对象然后使用,使用完之后,需要释放对象,存储回外部存储区。
public class DistributedLock {
public void lock() {
}
public void unlock() {
}
}
public class FileSharedStorage implements SharedStorage {
private String fileName;
public FileSharedStorage(String fileName) {
this.fileName = fileName;
}
@Override
public SingleTon load(Class cls) {
return null;
}
@Override
public void save(SingleTon sharedStorage, Class cls) {
}
}
public interface SharedStorage {
SingleTon load(Class cls);
void save(SingleTon sharedStorage, Class cls);
}
public class SingleTon {
private static final String sharedFileName = "file_name";
private static SingleTon instance;
private static DistributedLock lock = new DistributedLock();
private static SharedStorage storage = new FileSharedStorage(sharedFileName);
private SingleTon() {}
public static SingleTon getInstance() {
if (instance == null) {
lock.lock();
instance = storage.load(SingleTon.class);
}
return instance;
}
public synchronized void freeInstance() {
storage.save(this, SingleTon.class);
instance = null;
lock.unlock();
}
public void method() {}
}
多例模式怎么实现?
“单例模式”指一个类只能创建一个对象,那么类比多例模式,就是指一个类可以创建多个对象,但是这时候创建的对象个数,一般是有限制的。
类似于,我们要实现一个随机获取提供服务的后台服务器程序,每次返回的都是固定服务器对象列表中的某一个,达到将负载均衡的目的。
public class BeServer {
private int serverSequence;
private String serverAddr;
private static final int MAX_SERVER_COUNT = 5;
private static final Map<Integer, BeServer> serverMap = new HashMap<>();
static {
serverMap.put(1, new BeServer(1, "192.168.1.111:10001"));
serverMap.put(2, new BeServer(2, "192.168.1.112:10001"));
serverMap.put(3, new BeServer(3, "192.168.1.113:10001"));
}
private BeServer(int serverSequence, String serverAddr) {
this.serverSequence = serverSequence;
this.serverAddr = serverAddr;
}
public static BeServer getRandomBeServer() {
Random random = new Random();
int num = random.nextInt(MAX_SERVER_COUNT) + 1;
return serverMap.get(num);
}
}
这里,我们可以扩展一下,平时使用的logger是怎么实现的?针对同样的logger name,返回的是同一个logger对象实例,如果是不同的logger name,获取到的logger对象则是不同的。这其实也类似一种多实例模式。
public class Logger {
private static final Map<String, Logger> map = new ConcurrentHashMap<>();
private Logger() {
}
public static Logger getInstance(String loggerName) {
map.putIfAbsent(loggerName, new Logger());
return map.get(loggerName);
}
public void log() {
}
}
这种多例模式有点类似工厂模式,区别在于工厂模式创建的对象是不同子类的对象,多例模式创建的对象时同一个类的对象。
总结
单例模式看起来简单,但是想写出一个无bug的单例模式也不易。另外从单例模式,还可以扩展出线程单例,集群单例,多例模式等。
另外单例模式其实并不推荐使用,因为单例对OOP的特性支持并不友好,隐藏类之间的依赖关系,扩展性差,可测试性也不好,也不支持有参数的构造函数(一般的单例构造函数都是私有的)。
每一种设计模式,并不是是用的越多越好,不要为了使用设计模式而过渡滥用设计模式,不要为了设计而设计,要明白每一种设计模式是为了解决什么问题,为什么用,如何用,怎么用对用好。
通过举一反三,也可以应用到在学习和工作中,不是为了工作而工作,要抱着解决问题,提升自己的态度去工作,每一件事都不好做,舍我其谁!