单例模式你真的会了吗?(下篇)

    技术2022-07-10  185

    距离《单例模式上篇》写出去已经很久了,竟然久久没有更新下篇,这是庸俗人的普遍表现,只有开始,没有继续,也没有结束;干什么事都没有恒心,只有三天热度。要坚持啊!

    《单例模式上篇》描述了单例的几个核心问题:

    为什么要有单例?

    正确单例应该怎么写?

    典型的单例模式写法?

    接下来,我们来进阶一下,拓展一下单例的高级用法,所谓开拓思路,不亦乐乎嘛!

    单例模式的唯一性如何理解?线程唯一的单例怎么实现?如何实现集群模式下的单例?怎么实现“多例”模式?

    看着是不是有些头大,不要急,听我慢慢道来。

    单例模式的唯一性怎么理解

    我们先来看下单例的定义:“一个类只允许创建唯一一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。”

    一个类只允许创建唯一一个对象,这个唯一性的范围是什么?我们都知道程序运行的最小单位是线程,平时编写的一个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的特性支持并不友好,隐藏类之间的依赖关系,扩展性差,可测试性也不好,也不支持有参数的构造函数(一般的单例构造函数都是私有的)。

    每一种设计模式,并不是是用的越多越好,不要为了使用设计模式而过渡滥用设计模式,不要为了设计而设计,要明白每一种设计模式是为了解决什么问题,为什么用,如何用,怎么用对用好。

    通过举一反三,也可以应用到在学习和工作中,不是为了工作而工作,要抱着解决问题,提升自己的态度去工作,每一件事都不好做,舍我其谁!

    Processed: 0.010, SQL: 9