zl程序教程

您现在的位置是:首页 >  后端

当前栏目

漫谈模式之单例模式(通用写法的思考)

模式 通用 写法 思考 单例 漫谈
2023-06-13 09:15:27 时间

在之前的2篇博文漫谈模式之单例模式(多种实现方式的思考)漫谈模式之单例模式(破坏和防护的思考),已经讲解了单例的多种实现方式以及单例在反射、序列化反序列化以及克隆场景下的破坏和防护思考。本文也迎来了漫谈单例模式的最后篇章,如何写一个通用的单例?

在开展讲解之前,先回答一下,为什么要搞一个通用的写法呢

我们知道单例的写法有多种形式,每个人的风格不同。有一个通用的单例框架,可以有效保证风格的一致性。另外,鉴于开发人员的水平不一样,一个经过考验的通用单例模版,可以减少错误的引入。同时,也可以更好地去管理单例的使用。

当然,这并不是强制一定要这么做,使用D.C.L、内部Holder类或者枚举都是可以的。

接下来,我们在双重检查锁、CAS等实现单例的基础,看看通用单例的写法。

双重检查锁-通用写法

接口定义(并不是必须的)

定义一个获取实例接口,如:

抽象类

定义一个抽象的懒加载类。用于指定双重检查锁D.C.L的逻辑,同时指定一个create()方法用于具体子类创建对象使用。

使用示例

继承AbstractLazyInitializer,实现create()方法的内容,如:

然后通过如下方式完成调用即可。

AbstractLazyInitializer<SingletonExam> initializer =                                     new SingletonExam();//获取实例initializer.get();

这样,一个D.C.L的通用写法就弄好了。

CAS-通用写法

CAS我们通过原子类AtomicReference来做。

抽象类

定义一个抽象的懒加载类。维护一个可以原子更新的对象引用(初始值为null), 实现CAS的逻辑,同时指定一个create()方法用于具体子类创建对象使用。

使用示例

继承AbstractAtomicLazyInitializer,实现create()方法的内容,如:

然后通过如下方式完成调用即可。

AbstractAtomicLazyInitializer<SingletonExam> initializer =                                     new SingletonAtomicExam();//获取实例initializer.get();

某次执行的结果如下:

可以看到,多线程在处理CAS操作过程中,可能有多个线程执行到create()去创建了对象,但是,因为只有一个线程更新原子引用成功并返回,其余创建的对象是创建后丢弃的。

那么问题来了, 是否有可能只让create()方法执行一次?

答案是肯定的。

我们可以再增加一个当前类的原子引用,做一道防护在多线程环境下,只有设值成功的,才能去做create()操作。同时,CAS那部分,改成while循环,如下图所示:

CAS抽象类版本2

修改SingletonAtomicExam,继承自AbstractAtomicLazyInitializer2

然后,再执行一下测试代码,其结果如下:

我们可以看到只打印了一行************。也就是说create()方法确实只执行了一次。

上述代码在CAS不成功的时候,打印了do nothing,我们去掉一下。修改后的抽象类如下:

这样,CAS下只创建一次实例的抽象类也弄好了。

上述几种实现的思想,其实是之前看common-lang源码的时候摘记下来的,刚好引用到本文中来。common-lang的截图如下,有兴趣的读者可以去深入看看。

使用Guava的Suppliers.memoize

这是我比较喜欢的一种方式,也曾在项目中使用。

使用示例

编写一个实现Guava Supplier接口的示例

调用方式

Supplier<GuavaSupplierExam> initializer =       Suppliers.memoize(GuavaSupplierExam::new);//获取实例      initializer.get()

测试一下:

Suppliers.memoize实现本质

其实现本质是使用2个变量来完成双重检测锁D.C.L

至此,多种适合单例的懒加载方式就实现就介绍完成了。

整个单例模式的漫谈系列三部曲也完结了。如果读者有其它好的学习内容,也请一起交流学习。