设计模式-单例模式(下)

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 来存储对象类型和对象之间的对应关系,来控制对象的个数。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值