单例模式(下):如何设计实现⼀个集环境下的分布式单例模式?
上两节课中,我们针对单例模式,讲解了单例的应⽤场景、⼏种常见的代码实现和存在的问题,并粗略给出了替换单例模式的⽅法,⽐如⼯⼚模式、IOC 容器。今天,我们再进⼀步扩展延伸⼀下,⼀块讨论⼀下下⾯这⼏个问题:
如何理解单例模式中的唯⼀性?
如何实现线程唯⼀的单例?
如何实现集环境下的单例?
如何实现⼀个多例模式?
今天的内容稍微有点“烧脑”,希望你在看的过程中多思考⼀下。话不多说,让我们正式开始今天的学习吧!
如何理解单例模式中的唯⼀性?
⾸先,我们重新看⼀下单例的定义:“⼀个类只允许创建唯⼀⼀个对象(或者实例),那这个类就是⼀个单例类,这种设计模式就叫作单例设计模式,简称单例模式。”
定义中提到,“⼀个类只允许创建唯⼀⼀个对象”。那对象的唯⼀性的作⽤范围是什么呢?是指线程内只允许创建⼀个对象,还是指进程内只允许创建⼀个对象?答案是后者,也就是说,单例模式创建的对象是进程唯⼀的。这⾥有点不好理解,我来详细地解释⼀下。
我们编写的代码,通过编译、链接,组织在⼀起,就构成了⼀个操作系统可以执⾏的⽂件,也就是我们平时所说的“可执⾏⽂件”(⽐如Windows 下的 exe ⽂件)。可执⾏⽂件实际上就是代码被翻译成操作系统可理解的⼀组指令,你完全可以简单地理解为就是代码本⾝。
当我们使⽤命令⾏或者双击运⾏这个可执⾏⽂件的时候,操作系统会启动⼀个进程,将这个执⾏⽂件从磁盘加载到⾃⼰的进程地址空间(可以理解操作系统为进程分配的内存存储区,⽤来存储代码和数据)。接着,进程就⼀条⼀条地执⾏可执⾏⽂件中包含的代码。⽐如,当进程读到代码中的 User user = new User(); 这条语句的时候,它就在⾃⼰的地址空间中创建⼀个 user 临时变量和⼀个 User 对象。
进程之间是不共享地址空间的,如果我们在⼀个进程中创建另外⼀个进程(⽐如,代码中有⼀个 fork() 语句,进程执⾏到这条语句的时候会创建⼀个新的进程),操作系统会给新进程分配空间,并且将⽼进程地址空间的所有内容,重新拷贝⼀份到新进程的地址空间中,这些内容包括代码、数据(⽐如 user 临时变量、User 对象)。
所以,单例类在⽼进程中存在且只能存在⼀个对象,在新进程中也会存在且只能存在⼀个对象。⽽且,
这两个对象并不是同⼀个对象,这也就说,单例类中对象的唯⼀性的作⽤范围是进程内的,在进程间是不唯⼀的。
如何实现线程唯⼀的单例?
刚刚我们讲了单例类对象是进程唯⼀的,⼀个进程只能有⼀个单例对象。那如何实现⼀个线程唯⼀的单例呢?
我们先来看⼀下,什么是线程唯⼀的单例,以及“线程唯⼀”和“进程唯⼀”的区别。
“进程唯⼀”指的是进程内唯⼀,进程间不唯⼀。类⽐⼀下,“线程唯⼀”指的是线程内唯⼀,线程间可以不唯⼀。实际上,“进程唯⼀”还代表了线程内、线程间都唯⼀,这也是“进程唯⼀”和“线程唯⼀”的区别之处。这段话听起来有点像绕⼝令,我举个例⼦来解释⼀下。
假设 IdGenerator 是⼀个线程唯⼀的单例类。在线程 A 内,我们可以创建⼀个单例对象 a。因为线程内唯⼀,在线程 A 内就不能再创建新的 IdGenerator 对象了,⽽线程间可以不唯⼀,所以,在另外⼀个线程 B 内,我们还可以重新创建⼀个新的单例对象 b。
尽管概念理解起来⽐较复杂,但线程唯⼀单例的代码实现很简单,如下所⽰。在代码中,我们通过⼀个 HashMap 来存储对象,其中 key 是线程 ID,value 是对象。这样我们就可以做到,不同的线程对
应不同的对象,同⼀个线程只能对应⼀个对象。实际上,Java 语⾔本⾝提供了 ThreadLocal ⼯具类,可以更加轻松地实现线程唯⼀单例。不过,ThreadLocal 底层实现原理也是基于下⾯代码中所⽰的HashMap。
private static final ConcurrentHashMap<Long, IdGenerator> instances
=new ConcurrentHashMap<>();
private IdGenerator(){}
public static IdGenerator getInstance(){
Long currentThreadId = Thread.currentThread().getId();
instances.putIfAbsent(currentThreadId,new IdGenerator());
(currentThreadId);
}
public long getId(){
return id.incrementAndGet();
}
}
如何实现集环境下的单例?
刚刚我们讲了“进程唯⼀”的单例和“线程唯⼀”的单例,现在,我们再来看下,“集唯⼀”的单例。
⾸先,我们还是先来解释⼀下,什么是“集唯⼀”的单例。
我们还是将它跟“进程唯⼀”“线程唯⼀”做个对⽐。“进程唯⼀”指的是进程内唯⼀、进程间不唯⼀。“线程唯⼀”指的是线程内唯⼀、线程间不唯⼀。集相当于多个进程构成的⼀个集合,“集唯⼀”就相当于是进程内唯⼀、进程间也唯⼀。也就是说,不同的进程间共享同⼀个对象,不能创建同⼀个类的多个对象。
我们知道,经典的单例模式是进程内唯⼀的,那如何实现⼀个进程间也唯⼀的单例呢?如果严格按照不同的进程间共享同⼀个对象来实现,那集唯⼀的单例实现起来就有点难度了。
一个线程可以包含多个进程
具体来说,我们需要把这个单例对象序列化并存储到外部共享存储区(⽐如⽂件)。进程在使⽤这个单例对象的时候,需要先从外部共享存储区中将它读取到内存,并反序列化成对象,然后再使⽤,使⽤完成之后还需要再存储回外部共享存储区。
为了保证任何时刻,在进程间都只有⼀份对象存在,⼀个进程在获取到对象之后,需要对对象加锁,避免其他进程再将其获取。在进程使⽤完这个对象之后,还需要显式地将对象从内存中删除,并且释放对对象的加锁。
按照这个思路,我⽤伪代码实现了⼀下这个过程,具体如下所⽰:
private static IdGenerator instance;
private static SharedObjectStorage storage =FileSharedObjectStorage(/*⼊参省略,⽐如⽂件地址*/);
private static DistributedLock lock =new DistributedLock();
private IdGenerator(){}
public synchronized static IdGenerator getInstance()
if(instance == null){
lock.lock();
instance = storage.load(IdGenerator.class);
}
return instance;
}
public synchroinzed void freeInstance(){
storage.save(this, IdGeneator.class);
instance = null;//释放对象
lock.unlock();
}
public long getId(){
return id.incrementAndGet();
}
}
// IdGenerator使⽤举例
IdGenerator idGeneator = Instance();
long id = Id();
IdGenerator.freeInstance();
如何实现⼀个多例模式?
跟单例模式概念相对应的还有⼀个多例模式。那如何实现⼀个多例模式呢?
“单例”指的是,⼀个类只能创建⼀个对象。对应地,“多例”指的就是,⼀个类可以创建多个对象,但是个数是有限制的,⽐如只能创建3 个对象。如果⽤代码来简单⽰例⼀下的话,就是下⾯这个样⼦:
private long serverNo;
private String serverAddress;
private static final int SERVER_COUNT =3;
private static final Map<Long, BackendServer> serverInstances =new HashMap<>();
static{
serverInstances.put(1L,new BackendServer(1L,"192.134.22.138:8080"));
serverInstances.put(2L,new BackendServer(2L,"192.134.22.139:8080"));
serverInstances.put(3L,new BackendServer(3L,"192.134.22.140:8080"));
}
private BackendServer(long serverNo, String serverAddress){
this.serverNo = serverNo;
this.serverAddress = serverAddress;
}
public BackendServer getInstance(long serverNo){
(serverNo);
}
public BackendServer getRandomInstance(){
Random r =new Random();
int no = r.nextInt(SERVER_COUNT)+1;
(no);
}
}
实际上,对于多例模式,还有⼀种理解⽅式:同⼀类型的只能创建⼀个对象,不同类型的可以创建多个对象。这⾥的“类型”如何理解呢?
我们还是通过⼀个例⼦来解释⼀下,具体代码如下所⽰。在代码中,logger name 就是刚刚说的“类型”,同⼀个 logger name 获取到的对象实例是相同的,不同的 logger name 获取到的对象实例是不同的。
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());
(loggerName);
}
public void log(){
//...
}
}
//l1==l2, l1!=l3
Logger l1 = Instance("User.class");
Logger l2 = Instance("User.class");
Logger l3 = Instance("Order.class");
这种多例模式的理解⽅式有点类似⼯⼚模式。它跟⼯⼚模式的不同之处是,多例模式创建的对象都是同⼀个类的对象,⽽⼯⼚模式创建的是不同⼦类的对象,关于这⼀点,下⼀节课中就会讲到。实际上,它还有点类似享元模式,两者的区别等到我们讲到享元模式的时候再来分析。除此之外,实际上,枚举类型也相当于多例模式,⼀个类型只能对应⼀个对象,⼀个类可以创建多个对象。
重点回顾
好了,今天的内容到此就讲完了。我们来⼀块总结回顾⼀下,你需要掌握的重点内容。
今天的内容⽐较偏理论,在实际的项⽬开发中,没有太多的应⽤。讲解的⽬的,主要还是拓展你的思路,锻炼你的逻辑思维能⼒,加深你对单例的认识。
1. 如何理解单例模式的唯⼀性?
单例类中对象的唯⼀性的作⽤范围是“进程唯⼀”的。“进程唯⼀”指的是进程内唯⼀,进程间不唯⼀;“线程唯⼀”指的是线程内唯⼀,线程间可以不唯⼀。实际上,“进程唯⼀”就意味着线程内、线程间都唯⼀,这也是“进程唯⼀”和“线程唯⼀”的区别之处。“集唯⼀”指的是进程内唯⼀、进程间也唯⼀。
2. 如何实现线程唯⼀的单例?
我们通过⼀个 HashMap 来存储对象,其中 key 是线程 ID,value 是对象。这样我们就可以做到,不同的线程对应不同的对象,同⼀个线程只能对应⼀个对象。实际上,Java 语⾔本⾝提供了 ThreadLocal 并发⼯具类,可以更加轻松地实现线程唯⼀单例。
3. 如何实现集环境下的单例?
我们需要把这个单例对象序列化并存储到外部共享存储区(⽐如⽂件)。进程在使⽤这个单例对象的
时候,需要先从外部共享存储区中将它读取到内存,并反序列化成对象,然后再使⽤,使⽤完成之后还需要再存储回外部共享存储区。为了保证任何时刻在进程间都只有⼀份对象存在,⼀个进程在获取到对象之后,需要对对象加锁,避免其他进程再将其获取。在进程使⽤完这个对象之后,需要显式地将对象从内存中删除,并且释放对对象的加锁。
4. 如何实现⼀个多例模式?
“单例”指的是⼀个类只能创建⼀个对象。对应地,“多例”指的就是⼀个类可以创建多个对象,但是个数是有限制的,⽐如只能创建 3个对象。多例的实现也⽐较简单,通过⼀个 Map 来存储对象类型和对象之间的对应关系,来控制对象的个数。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。