1、如何理解单例模式的唯一性:
我们重新看一下单例的定义:“一个类只允许创建唯一一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。”
定义中提到,“一个类只允许创建唯一一个对象”。那对象的唯一性的作用范围是什么呢?是指线程内只允许创建一个对象,还是指进程内只允许创建一个对象?答案是后者,也就是说,单例模式创建的对象是进程唯一的。
2、如何实现线程唯一的单例:
“进程唯一”指的是进程内唯一,进程间不唯一。类比一下,“线程唯一”指的是线程内唯一,线程间可以不唯一。实际上,“进程唯一”还代表了线程内、线程间都唯一,这也是“进程唯一”和“线程唯一”的区别之处。
假设 IdGenerator 是一个线程唯一的单例类。在线程 A 内,我们可以创建一个单例对象 a。因为线程内唯一,在线程 A 内就不能再创建新的 IdGenerator 对象了,而线程间可以不唯一,所以,在另外一个线程 B 内,我们还可以重新创建一个新的单例对象 b。
我们通过一个 HashMap 来存储对象,其中 key 是线程 ID,value 是对象。这样我们就可以做到,不同的线程对应不同的对象,同一个线程只能对应一个对象。实际上,Java 语言本身提供了 ThreadLocal 工具类,可以更加轻松地实现线程唯一单例。不过,ThreadLocal 底层实现原理也是基于下面代码中所示的 HashMap。
/**
* 实现一个线程之间单例的方法
* @author PengMvc
*
*/
public class SingtonAmongThread {
// 存储实例化好的对象
private static ConcurrentHashMap<Long, SingtonAmongThread> alreadyCreateInstance = new ConcurrentHashMap<>();
// 私有化构造方法,不让外界去new对象
private SingtonAmongThread () {}
public static SingtonAmongThread getInstance() {
// 线程ID
long id = Thread.currentThread().getId();
// key thread id value:SingtonAmongThread
alreadyCreateInstance.putIfAbsent(id, new SingtonAmongThread());
return alreadyCreateInstance.get(id);
}
public static void querySingletonFromMap() {
alreadyCreateInstance.forEach((key,value)->{
System.out.println(key+"-->"+value);
});
}
}
/**
* 测试类
* @author PengMvc
*
*/
public class TestAmongThreadSingleton {
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExcutor = CommonThreadPoolExcutor.getThreadPoolExcutor();
for(int i=0;i<11;i++) {
threadPoolExcutor.execute(()->{
SingtonAmongThread.getInstance();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
}
SingtonAmongThread.querySingletonFromMap();
}
}
3、如何实现集群环境下的单例:
什么是“集群唯一”的单例?
将它跟“进程唯一”“线程唯一”做个对比。“进程唯一”指的是进程内唯一、进程间不唯一。“线程唯一”指的是线程内唯一、线程间不唯一。集群相当于多个进程构成的一个集合,“集群唯一”就相当于是进程内唯一、进程间也唯一。也就是说,不同的进程间共享同一个对象,不能创建同一个类的多个对象。
如果严格按照不同的进程间共享同一个对象来实现,那集群唯一的单例实现起来就有点难度了,我们需要把这个单例对象序列化并存储到外部共享存储区(比如文件)。进程在使用这个单例对象的时候,需要先从外部共享存储区中将它读取到内存,并反序列化成对象,然后再使用,使用完成之后还需要再存储回外部共享存储区。
为了保证任何时刻,在进程间都只有一份对象存在(并不是同一个对象这个是和反序列化的区别),一个进程在获取到对象之后,需要对对象加锁,避免其他进程再将其获取。在进程使用完这个对象之后,还需要显式地将对象从内存中删除,并且释放对对象的加锁,代码如下:
/**
* 集群级别的单例
* 举列:A B系统进行交互(跨进程)需要确保实例单一
* 那么我们在A系统创建一个单例实例后,将此实例存储在redis里面;
* 接下来在B系统的业务代码中如果需要用到该实例,那么我们直接从redis里去
* 获取已经创建好的单例,当我们在B系统中的业务流程已经确定结束后,我们可以
* 直接将该实例从redis中删除,避免对redis内存的占用。
* 伪代码如下
* @author PengMvc
*
*/
public class ClusterSingleton {
private static ClusterSingleton clusterSingleton;
// 锁对象
private static ReentrantLock lock = new ReentrantLock();
/**
* 加锁 创建对象
* @return clusterSingleton
*/
public static ClusterSingleton createInstance() {
if(clusterSingleton == null) {
// 加锁
lock.lock();
// 创建实例
clusterSingleton = new ClusterSingleton();
// 将对象存在redis中
loadInstanceToRedis(clusterSingleton);
}
return clusterSingleton;
}
/**
* 将对象从内存中删去
* 且释放锁
*/
public static void freeClusterSingleton() {
// 将对象从redis中删除
removeInstanceFromRedis(clusterSingleton);
// 释放对象
clusterSingleton = null;
// 释放锁
lock.unlock();
}
}
/**
* 测试类
* @author PengMvc
*
*/
public class ClusterSingletonTest {
public static void main(String[] args) {
// A系统创建实例,且将实例存入redis
ClusterSingleton instance = ClusterSingleton.createInstance();
//instance.findInfo();A系统ִ执行业务逻辑
// 通过B系统提供的接口,调用B系统的接口
// 以下进行B系统的业务处理,可以从redis中获取该实例
// B系统业务结束后,执行以下方法
ClusterSingleton.freeClusterSingleton();
}
}
4、如何实现一个多例模式:
单例”指的是,一个类只能创建一个对象。对应地,“多例”指的就是,一个类可以创建多个对象,但是个数是有限制的,比如只能创建 3 个对象。如果用代码来简单示例一下的话,就是下面这个样子:
/**
* 多例创建
* @author PengMvc
*
*/
public class BackendServer {
private static final Map<Long,BackendServer> serverMap = new HashMap<>(3);
// 服务器编号
private Long serverNo;
// 服务器IP
private String serverIp;
static {
serverMap.put(1L, new BackendServer(1L,"127.0.0.1:8080"));
serverMap.put(2L, new BackendServer(2L,"127.0.0.2:8080"));
serverMap.put(3L, new BackendServer(3L,"127.0.0.3:8080"));
}
// 私有化构造方法避免外部new
private BackendServer(Long serverNo, String serverIp) {
this.serverNo = serverNo;
this.serverIp = serverIp;
}
public static BackendServer getBackendServer(Long serverNo) {
if(serverNo ==null || serverMap.get(serverNo)==null) {
throw new ExceptionInInitializerError("获取服务器异常");
}
return serverMap.get(serverNo);
}
public Long getServerNo() {
return serverNo;
}
public void setServerNo(Long serverNo) {
this.serverNo = serverNo;
}
public String getServerIp() {
return serverIp;
}
public void setServerIp(String serverIp) {
this.serverIp = serverIp;
}
}
public class BackendServerTest {
public static void main(String[] args) {
BackendServer server = BackendServer.getBackendServer(3l);
System.out.println(server.getServerNo()+"-->"+server.getServerIp());
}
}
这种多例模式的理解方式有点类似工厂模式。它跟工厂模式的不同之处是,多例模式创建的对象都是同一个类的对象,而工厂模式创建的是不同子类的对象。
5、总结如下:
5.1. 如何理解单例模式的唯一性?单例类中对象的唯一性的作用范围是“进程唯一”的。“进程唯一”指的是进程内唯一,进程间不唯一;“线程唯一”指的是线程内唯一,线程间可以不唯一。实际上,“进程唯一”就意味着线程内、线程间都唯一,这也是“进程唯一”和“线程唯一”的区别之处。“集群唯一”指的是进程内唯一、进程间也唯一。
5.2. 如何实现线程唯一的单例?我们通过一个 HashMap 来存储对象,其中 key 是线程 ID,value 是对象。这样我们就可以做到,不同的线程对应不同的对象,同一个线程只能对应一个对象。实际上,Java 语言本身提供了 ThreadLocal 并发工具类,可以更加轻松地实现线程唯一单例。
5.3. 如何实现集群环境下的单例?我们需要把这个单例对象序列化并存储到外部共享存储区(比如文件)。进程在使用这个单例对象的时候,需要先从外部共享存储区中将它读取到内存,并反序列化成对象,然后再使用,使用完成之后还需要再存储回外部共享存储区。为了保证任何时刻在进程间都只有一份对象存在,一个进程在获取到对象之后,需要对对象加锁,避免其他进程再将其获取。在进程使用完这个对象之后,需要显式地将对象从内存中删除,并且释放对对象的加锁。
5.4. 如何实现一个多例模式?“单例”指的是一个类只能创建一个对象。对应地,“多例”指的就是一个类可以创建多个对象,但是个数是有限制的,比如只能创建 3 个对象。多例的实现也比较简单,通过一个 Map 来存储对象类型和对象之间的对应关系,来控制对象的个数。