目录
第二种: 同一类型只能创建一个对象,不同类型可以创建多个对象
一个类只允许创建唯一一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫做单例设计模式,简称单例模式。
这里提到的唯一性是针对进程而非线程,我们编写的代码通过编译、链接后组织在一起,构成了一个操作系统可以执行的文件,也就是我们平时所说的 “可执行文件” (比如Windows下的exe文件)。可执行文件实际上就是代码被翻译成操作系统可理解的一组指令。
当使用命令行或者双击运行这个可执行文件时,操作系统会启动一个进程,将这个执行文件从磁盘加载到自己的进程地址空间(可以理解成操作系统为进程分配的内存存储区,用来存储代码和数据)。接着,进程就一条一条地执行可执行文件中包含的代码。比如,当进程读到代码中的 User user = new User(); 这条语句的时候,它就在自己的地址空间中创建一个user临时变量和一个User对象。
进程之间是不共享地址空间的,如果我们在一个进程中创建另外一个进程,操作系统会给新进程分配新的地址空间,并且将老进程地址空间的所有内容,重新拷贝一份到新进程的地址空间中,这些内容包括代码、数据等。
所以,单例类在老进程中存在且只能存在一个对象,在新进程中也会存在且只能存在一个对象。而且,这两个对象并不是同一个对象,这也就是说,单例类中对象的唯一性的作用范围是进程内的,在进程间是不唯一的。
先理解下面这段话
线程唯一 != 进程唯一
进程唯一 => 线程唯一
可以使用 ConcurrentHashMap 作为底层数据结构存储对象,其中 key 是线程Id,value是对象。
- public class IdGenerator {
-
- /** id生成器 */
- private AtomicLong id = new AtomicLong(1);
- /** 存储每个线程id生成器的map容器 */
- private static final ConcurrentHashMap
instances - = new ConcurrentHashMap<>();
-
- /** 私有构造器,不允许外部 new对象 */
- private IdGenerator() {
- }
- /** 获取当前线程的id生成器对象 */
- public static IdGenerator getInstance() {
- long threadId = Thread.currentThread().getId();
- instances.putIfAbsent(threadId, new IdGenerator());
- return instances.get(threadId);
- }
- /** 获取id */
- public long getId() {
- return id.getAndIncrement();
- }
-
- }
这样就可以做到不同的线程对应不同的对象,同一个线程只能对应一个对象。实际上,Java语言本身提供了 ThreadLocal 工具类,可以更加轻松地实现线程唯一单例。
经典的单例模式是进程内唯一的。所谓集群环境下的单例也就是进程间唯一,这里可以采用将单例对象序列化并存储到外部共享存储区(比如文件)。进程在使用这个单例对象的时候,需要先从外部共享存储区中将它读取到内存,并反序列化成对象,然后再使用,使用完成之后还需要再存储回外部共享存储区。
为了保证任何时刻,在进程间都只有一份对象存在,一个进程在获取到对象之后,需要进行对象加锁,避免其他进程再将其获取。在进程使用完这个对象之后,还需要显式地将对象从内存中删除,并且释放对对象的加锁。
伪代码如下:
- public class DistributedIdGenerator {
- /** id生成 */
- private AtomicLong id = new AtomicLong(1);
- private static DistributedIdGenerator instance;
- /** 进程间共享对象外部存储 */
- @Resource
- private SharedObjectStorage storage;
- /** 分布式锁 */
- @Resource
- private DistributedLock lock;
-
- private DistributedIdGenerator() {
- }
- /** 获取进程间共享的DistributedIdGenerator对象 */
- public synchronized static DistributedIdGenerator getInstance() {
- if (instance == null) {
- lock.lock();
- instance = storage.load(DistributedIdGenerator.class);
- }
- return instance;
- }
- /** 当前进程使用完后需释放当前实例 */
- public synchronized void freeInstance() {
- storage.save(this, DistributedIdGenerator.class);
- instance = null;
- lock.unlock();
- }
-
- public long getId() {
- return id.getAndIncrement();
- }
-
- }
这里的多例模式可以理解成两种:
- 一个类可以创建有限的多个对象
- 同一类型只能创建一个对象,不同类型可以创建多个对象
可以采取随机数或者是根据特征id进行分片。
- public class BackupServer {
-
- private long serverNo;
- private String serverAddress;
-
- public BackupServer(long serverNo, String serverAddress) {
- this.serverNo = serverNo;
- this.serverAddress = serverAddress;
- }
- /** 服务器数量 */
- private static final int SERVER_COUNT = 3;
- /** 存储服务器实例 */
- private static final Map
SERVER_MAP = new HashMap<>(); - /** 初始化服务器实例 */
- static {
- for (long i = 1; i <= SERVER_COUNT; i++) {
- SERVER_MAP.put(i, new BackupServer(i, "192.168.22." + i +":8080"));
- }
- }
-
- public BackupServer getInstance(long serverNo) {
- return SERVER_MAP.get(serverNo);
- }
- /** 随机获取服务器 */
- public BackupServer getRandomInstance() {
- Random random = new Random();
- long no = random.nextInt(SERVER_COUNT) + 1;
- return SERVER_MAP.get(no);
- }
-
- }
- public class LogInstanceDemo {
-
- private static final ConcurrentHashMap
instances - = new ConcurrentHashMap<>();
-
- private LogInstanceDemo() {
- }
-
- public static LogInstanceDemo getInstance(String loggerName) {
- instances.putIfAbsent(loggerName, new LogInstanceDemo());
- return instances.get(loggerName);
- }
-
- }
-
- // log1 == log2, log1 != log3 && log2 != log3
- LogInstanceDemo log1 = LogInstanceDemo.getInstance("User.class");
- LogInstanceDemo log2 = LogInstanceDemo.getInstance("User.class");
- LogInstanceDemo log3 = LogInstanceDemo.getInstance("Person.class");
这种多例模式类似工厂模式,但它跟工厂模式的不同之处在于,多例模式创建的对象都是同一个类的对象,而工厂模式创建的是不同子类的对象。
上文中讲述单例唯一性的作用范围是进程,实际上,对于Java语言来说,单例类对象的唯一性的作用范围并非进程,而是类加载器(ClassLoader),这是因为不同类加载器之间命名空间不一样,不同的类加载器加载出来的类实例是不一样的,所以Java语言是类加载器内唯一。