大家好,欢迎关注极客架构师,极客架构师,专注架构师成长,我是码农老吴。

本期是《架构师基本功之设计模式》的第8期,我将基于单例模式(singleton pattern)及Google的guava cache缓存组件,打造高并发,线程安全的本地缓存。
由于单例模式本身的简单性,还有就是单例模式一般不是通过重构代码获得的,而是直接完成,所以,本次分享的基本思路,不同于前面几个设计模式。这次我提前分享单例模式的定义及分析等概念性内容,然后进行案例和代码讲解,两者的顺序进行了对调。
一提到单例模式的代码实现方式,各种教程,形形色色,林林总总,你说两三种,我就说四五种,你说四五种,我就说个七八种,更有甚者,会介绍十多种,大有愈演愈烈,高度内卷的趋势。然而千变万化,不离其宗,我们要透过迷雾直达本质,单例模式根本上只有两种,其他的都是语法上的小技巧而已,大家不要本末倒置,舍本逐末。
结论先行
根据REIS分析模型,对单例模式进行分析,单例模式包含两种角色,单例服务方角色和单例客户方角色。它的宗旨是,单例服务方角色,通过私有化的构造函数,静态的类属性以及静态的getInstance()方法等措施,确保所有单例客户方,都只能获取到它的唯一一个对象。该模式的研究重点是对象实例化的时机问题,可以分为两种实例化方式,提前实例化和延迟实例化。
基本思路
高并发互联网项目的缓存架构
单例模式定义
使用REIS模型分析单例模式
提前实例化:第一版代码(属性初始化,推荐方案)
提前实例化:第二版代码(静态块初始化)
延迟实例化:第一版代码(非线程安全,常见方案)
延迟实例化:第一版代码优化(线程安全,低效)
延迟实例化:第一版代码再优化(线程安全,双重检测,高效,推荐方案)
延迟实例化:第二版代码(线程安全,双重检测,可重入锁,高效)
延迟实例化:第三版代码(线程安全,静态内部类)
单例模式需要深入思考的点
单例模式通用代码
高并发互联网项目的缓存架构
互联网项目,尤其是电商行业的项目,常常被称为是高并发的软件项目,往往对QPS(或者TPS)吞吐量指标和TP99等性能指标有很高的要求。特别是618,双十一等重大节日,零点大促时,流量峰值,有可能是平时的几倍,甚至几十倍。一不小心,就有可能系统崩溃。而要让系统能达到这些吞吐量和性能指标,除了在大促之前的容器扩展之外,还有一个很重要的方面,就是在进行系统架构设计,技术选型时。充分发挥和挖掘缓存的潜力。可以这么说,缓存是高并发系统的银弹,性能不够,缓存来凑。
互联网项目常见的缓存体系,有客户端缓存(浏览器端),本地缓存(堆缓存,堆外缓存,磁盘缓存),分布式缓存集群(redis)等。这些,我们今天不具体展开了,后面我会有专门的分享,欢迎大家关注。今天,我们聚焦在本地缓存上。看看如何结合单例模式和Google的guava Cache缓存组件,打造高并发,线程安全的本地缓存。
我们今天单刀直入,从单例模式的定义,开始讲起。
单例模式(Singleton pattern)定义
Ensure a class only has one instance, and provide a global point of access to it.
—— Gof《Design Patterns: Elements of Reusable Object-Oriented Software》
中文:
保证一个类,仅有一个实例,并提供一个访问它的全局访问点。
这个定义有两个关键词,一个实例和一个全局访问点
一个实例(one instance)
单例模式,顾名思义,就是单个实例。至于为什么只需要一个实例,定义里面并没有说,后面我们再展开说明。
一个全局访问点(a global point of access to it)
这个词如何理解呢?在常见的编程语言里面,全局访问点这个词很少见。但是,正常情况下,如果我们要访问一个实例,或者叫对象,应该如何访问呢。很明显,就是调用这个实例或者叫对象的变量名称。那么“全局访问点”就可以解释为“全局的变量名称”,有过其他开发语言经验的朋友,脑子里面可能就会浮现出“全局变量”这四个字。是的,你想得没错,就是全局变量。
但是java语言里面,并没有全局变量的概念。只能通过一定的编程技巧去模拟。而最常见的一种做法,就是通过类里面的static关键字来模拟全局变量。在后面的案例中,大家就能看到它。
下面我们用REIS模型,对单例模式进行分析,大家就会理解的更透彻一些。
使用REIS模型分析单例模式
REIS模型是我总结的分析设计模式的一种方法论,主要包括场景(scene),角色(role),交互(interaction),效果(effect)四个要素。
场景(Scene)
场景,也就是我们在什么情况下,遇到了什么问题,需要使用某个设计模式。
当出现以下情况时,可能需要使用单例模式。
当某个类的对象,在系统中是独一无二的,只可以存在一个对象时,就需要使用单例模式,常见的有缓存管理器,线程池,数据库连接,日志管理器,配置参数,驱动程序等。今天我们就是要实现本地缓存管理器。
角色(Role)
角色,一般为设计模式出现的类,或者对象。每种角色有自己的职责。
单例模式,结构简单,里面只有两个角色,单例服务方角色,单例客户方角色。
单例服务方角色( singleton service role):单例服务方角色,就像概念里面所描述的那样,它是一个类,一个只允许创建一个实例的类,为了做到这一点,它把创建实例的权限对外屏蔽了,只有它自己可以使用。而为了让别人能访问到它的实例,它专门提供了一个static方法(也就是概念里面的全局访问点),用于返回自己的实例。它的职责概括如下:
私有构造函数:确保类的对象不能从外部创建,只能被它自己创建。静态类属性,类型为它自身:通过静态属性,从语法角度确保这个类的对象,在JVM中只存在一个对象。静态getInstance()方法,返回类型为它自身:这个静态方法,就是这个类的唯一一个全局访问点,通过这个静态方法,单例客户方可以获取单例服务方的唯一一个对象。单例客户方角色(singleton client role):单例客户方,是需要访问单例服务方角色的任何对象,而且只能通过单例服务方角色提供的统一的全局访问点(也就是getInstance()方法),来获取单例服务方角色的唯一一个对象。任何企图绕过全局访问点,获取单例对象的单例客户方,都是不友好的,都应该被禁止,都需要后果自负。如果能绕过去,也间接说明单例服务方角色,没有通过有效的措施,确保只有一个全局访问点。
交互(interaction)
交互,是指设计模式中,各种角色是如何交互的,一般用UML中的序列图,活动图来表示。简单地说就是角色之间是如何配合,完成设计模式的使命的。
单例模式,虽然结构简单,交互看上去似乎也很简单,但是这里面另有蹊跷,大有文章,值得我们重视。
对于单例模式,非常重要的一个关键点就是,何时完成对象的实例化。单例角色的实例化(initialization)方式,根据实例化的时机,可以分为以下两种。
提前实例化(Early initialization or Eager initialization)方式
提前实例化,英文叫early initialization 或者 eager initialization,early大家都很明白,eager,中文意思是热切的,渴望的,渴求的,表达了一种急不可耐的情绪,中文文章中常常把这种方式,形象的称之为饿汉方式。
那么,到底要提前多少,愿望到底有多强烈呢,通常是在类加载的时候,不同的实现方式,稍有不同,但是底线还是比较明确的,就是在单例客户方首次访问getInstance()方法之前,也就是确保在客户需要是的时候,已经提前完成了对象初始化。
这种方式,优点是由于对象已经提前初始化了,所以无需考虑线程安全问题,代码比较简洁。而缺点也很明显,就是不管单例客户方是否使用,都提前创建好对象,可能会有一定的资源浪费,但是因为本身是单例,所以浪费的资源往往有限,常常可以忽略不计。所以,提前实例化,一般是首选方案,所以我们后面的案例,首选讲解的就是这种方式。
延迟实例化(Lazy initialization)方式
延迟实例化,英文是Lazy initialization,中文文章中,常常把这种方式,形象地比喻为懒汉方式,和上面的饿汉方式,形成了强烈的对比,表达了一种慵懒的,得过且过,能不做,最好不做的情绪。
那么,到底延迟到什么时候呢,很明显,只有到最后一刻,不得不做的地步,懒汉才会行动,deadline永远是第一生产力。具体的说,这最后一刻,就是单例客户方首次调用getInstance()方法的那一刻。
这种方式,优点就是不会造成资源浪费。但是缺点甚至说是风险是存在的,一个不小心,轻则效率低下,重则违背单例模式的宗旨,创建了多个对象。看来还是老话说得好,成功细中取, 富贵险中求。要想有收获,就需要一定的付出。由于需要考虑多线程安全问题,这里面常常需要使用多线程的相关技术,代码比较复杂一些,需要消耗一些码农的脑细胞,也常常是面试的重要内容。
效果(effect)
效果,使用该设计模式之后,达到了什么效果,有何意义,当然,也可以说说它的缺点,或者风险。
单例模式的宗旨只有确保一个类,只有一个对象,这样造成的客观效果就是数据统一,节省资源。我就不细说了。
前面的定义,概念,讲解完毕,下面我们开始编写代码。切身体会什么是单例模式。
前面我们已经提到过,提前实例化因为不需要考虑多线程问题,所以代码相对来说,比较简单,我们就从它开始讲起。
提前实例化:第一版代码(属性初始化,推荐方案)
POM文件
导入guava cache 组件
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>DesignPatterns</artifactId> <groupId>com.geekarchitect.patterns</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>singleton-pattern</artifactId> <dependencies> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>23.0</version> </dependency> </dependencies> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> </project>
ILocalCache:本地缓存接口
缓存接口,比较高频的两个操作是加入缓存和从缓存中取出。
package com.geekarchitect.patterns.singleton.demo01; /** * 本地缓存接口 * * @author 极客架构师@吴念 * @createTime 2022/5/30 */ public interface ILocalCache { void put(String key, String value); String get(String key); }
LocalCacheManagerV21:第一版代码(属性初始化)
要点提示:
私有构造函数:确保类不能从外部创建对象,只能被它自己创建。静态getInstance()方法,返回类型为它自身:这个静态方法,就是这个类的唯一一个全局访问点,通过这个静态方法,单例客户方,可以获取这个单例服务方的唯一对象。静态类属性,类型为它自身:通过静态属性LOCAL_CACHE_MANAGER_V_21,类型为LocalCacheManagerV11,确保这个类的对象,在JVM中只存在一个。静态属性LOCAL_CACHE_MANAGER_V_21的初始化方式:这个属性,是在定义的时候,直接初始化,也就是属性初始化。package com.geekarchitect.patterns.singleton.demo02; import com.geekarchitect.patterns.singleton.demo01.ILocalCache; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author 极客架构师@吴念 * @createTime 2022/5/30 */ public class LocalCacheManagerV21 implements ILocalCache { private static final Logger LOG = LoggerFactory.getLogger(LocalCacheManagerV21.class); private static final LocalCacheManagerV21 LOCAL_CACHE_MANAGER_V_21 = new LocalCacheManagerV21(); private final Cache<String, String> guavaCache; private LocalCacheManagerV21() { LOG.info("提前实例化:第一版代码(属性初始化)"); LOG.info("初始化缓存"); guavaCache = CacheBuilder.newBuilder().build(); } public static LocalCacheManagerV21 getInstance() { return LOCAL_CACHE_MANAGER_V_21; } @Override public void put(String key, String value) { guavaCache.put(key, value); } @Override public String get(String key) { return guavaCache.getIfPresent(key); } }
TestLocalCacheManagerV02: 测试类
package com.geekarchitect.patterns.singleton.demo02; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author 极客架构师@吴念 * @createTime 2022/5/30 */ public class TestLocalCacheManagerV02 { private static final Logger LOG = LoggerFactory.getLogger(TestLocalCacheManagerV02.class); public static void main(String[] args) { TestLocalCacheManagerV02 testLocalCacheManager = new TestLocalCacheManagerV02(); testLocalCacheManager.demo01(); } public void demo01() { LocalCacheManagerV21 localCacheManagerV2101 = LocalCacheManagerV21.getInstance(); LocalCacheManagerV21 localCacheManagerV2102 = LocalCacheManagerV21.getInstance(); localCacheManagerV2101.put("1", "北京"); LOG.info("localCacheManagerV2101 == localCacheManagerV2102 is {}", (localCacheManagerV2101 == localCacheManagerV2102)); LOG.info("localCacheManagerV2101.get('1')={} ", (localCacheManagerV2101.get("1"))); LOG.info("localCacheManagerV2102.get('2')={} ", (localCacheManagerV2102.get("1"))); } public void demo02() { LocalCacheManagerV22 localCacheManagerV2201 = LocalCacheManagerV22.getInstance(); LocalCacheManagerV22 localCacheManagerV2202 = LocalCacheManagerV22.getInstance(); localCacheManagerV2201.put("1", "北京"); LOG.info("localCacheManagerV2201 == localCacheManagerV2202 is {}", (localCacheManagerV2201 == localCacheManagerV2202)); LOG.info("localCacheManagerV2201.get('1')={} ", (localCacheManagerV2201.get("1"))); LOG.info("localCacheManagerV2202.get('2')={} ", (localCacheManagerV2202.get("1"))); } }
运行结果
头脑风暴
这版代码,通过对静态类属性的直接初始化,确保单例客户方在调用getInstance()方法之前,就完成了对象的实例化。单例客户方可以放心大胆使用单例对象,而不用考虑多线程问题。所以使用率比较高,是推荐方案。
但是,对于静态类属性的初始化,还有另外一种初始化方式,就是静态块方式。我们继续看第二版代码。
提前实例化:第二版代码(静态块初始化)
LocalCacheManagerV22:第二版代码(静态块初始化)
要点提示:
静态属性localCacheManagerV22的初始化方式:这个属性,不是在定义的时候初始化,而是在static块中进行初始化。package com.geekarchitect.patterns.singleton.demo02; import com.geekarchitect.patterns.singleton.demo01.ILocalCache; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author 极客架构师@吴念 * @createTime 2022/5/30 */ public class LocalCacheManagerV22 implements ILocalCache { private static final Logger LOG = LoggerFactory.getLogger(LocalCacheManagerV22.class); private final Cache<String, String> guavaCache; private static final LocalCacheManagerV22 localCacheManagerV22; static { LOG.info("提前实例化:第二版代码(静态块初始化,推荐方案)"); localCacheManagerV22 = new LocalCacheManagerV22(); } private LocalCacheManagerV22() { LOG.info("初始化缓存"); guavaCache = CacheBuilder.newBuilder().build(); } public static LocalCacheManagerV22 getInstance() { return localCacheManagerV22; } @Override public void put(String key, String value) { guavaCache.put(key, value); } @Override public String get(String key) { return guavaCache.getIfPresent(key); } }
运行结果
头脑风暴
这版代码,在static块中初始化静态类属性,代码会更灵活一些。
提前实例化方式实现的单例模式,有其固有的缺点,那就是可能会造成资源浪费,但由于是单例,所以一般浪费的都不多,所以它可以满足项目的大部分情况。但是对于追求技术精益求精,眼里不容沙子的研发人员,就需要精通下面的延迟实例化方式了,我们继续。
延迟实例化:第一版代码(非线程安全)
LocalCacheManagerV11:第一版(非线程安全)
要点提示:
私有构造函数:确保类不能从外部创建对象,只能被它自己创建。静态类属性,类型为它自身:通过静态属性localCacheManagerV11,类型为LocalCacheManagerV11,从语法角度确保这个类的对象,在JVM中只存在一个。静态getInstance()方法,返回类型为它自身:这个静态方法,就是这个类的唯一一个全局访问点,通过这个静态方法,单例客户方,可以获取这个单例服务的唯一对象。package com.geekarchitect.patterns.singleton.demo01; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * 本地缓存管理器 * * @author 极客架构师@吴念 * @createTime 2022/5/30 */ public class LocalCacheManagerV11 implements ILocalCache { private static final Logger LOG = LoggerFactory.getLogger(LocalCacheManagerV11.class); private static Cache<String, String> guavaCache = null; private static LocalCacheManagerV11 localCacheManagerV11 = null; private LocalCacheManagerV11() { initCache(); } public static LocalCacheManagerV11 getInstance() { if (null == localCacheManagerV11) { LOG.info("延迟实例化:第一版代码(非线程安全)"); localCacheManagerV11 = new LocalCacheManagerV11(); } return localCacheManagerV11; } private void initCache() { LOG.info("初始化缓存"); guavaCache = CacheBuilder.newBuilder().build(); } @Override public void put(String key, String value) { guavaCache.put(key, value); } @Override public String get(String key) { return guavaCache.getIfPresent(key); } }
TestLocalCacheManagerV01:测试类
package com.geekarchitect.patterns.singleton.demo01; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author 极客架构师@吴念 * @createTime 2022/5/30 */ public class TestLocalCacheManagerV01 { private static final Logger LOG = LoggerFactory.getLogger(TestLocalCacheManagerV01.class); public static void main(String[] args) { TestLocalCacheManagerV01 testLocalCacheManagerV01 = new TestLocalCacheManagerV01(); testLocalCacheManagerV01.demo01(); } public void demo01() { LocalCacheManagerV11 localCacheManagerV1101 = LocalCacheManagerV11.getInstance(); LocalCacheManagerV11 localCacheManagerV1102 = LocalCacheManagerV11.getInstance(); localCacheManagerV1101.put("1", "北京"); LOG.info("localCacheManagerV1101 == localCacheManagerV1102 is {}", (localCacheManagerV1101 == localCacheManagerV1102)); LOG.info("localCacheManagerV1101.get('1')={} ", (localCacheManagerV1101.get("1"))); LOG.info("localCacheManagerV1102.get('2')={} ", (localCacheManagerV1102.get("1"))); } public void demo02() { LocalCacheManagerV12 localCacheManagerV1201 = LocalCacheManagerV12.getInstance(); LocalCacheManagerV12 localCacheManagerV1202 = LocalCacheManagerV12.getInstance(); localCacheManagerV1201.put("1", "北京"); LOG.info("localCacheManagerV1201 == localCacheManagerV1202 is {}", (localCacheManagerV1201 == localCacheManagerV1202)); LOG.info("localCacheManagerV1201.get('1')={} ", (localCacheManagerV1201.get("1"))); LOG.info("localCacheManagerV1202.get('2')={} ", (localCacheManagerV1202.get("1"))); } public void demo03() { LocalCacheManagerV13 localCacheManagerV1301 = LocalCacheManagerV13.getInstance(); LocalCacheManagerV13 localCacheManagerV1302 = LocalCacheManagerV13.getInstance(); localCacheManagerV1301.put("1", "北京"); LOG.info("localCacheManagerV1301 == localCacheManagerV1302 is {}", (localCacheManagerV1301 == localCacheManagerV1302)); LOG.info("localCacheManagerV1301.get('1')={} ", (localCacheManagerV1301.get("1"))); LOG.info("localCacheManagerV1302.get('2')={} ", (localCacheManagerV1302.get("1"))); } }
运行结果
头脑风暴
这版代码,就是典型的延迟实例化的单例模式,存储单例对象的静态属性localCacheManagerV11,只有在单例客户方第一次调用静态getInstance()方法时,才会进行实例化。
这是一种比较常见的方式,在一般的系统中已经够用了。但是,在多线程环境里面,也就是单例客户方是分布在多个线程里面,并发调用,上面的代码就会出现问题。什么问题呢?
当多个处于不同线程的单例客户方并发调用getInstance()方法,可能会发生,创建多个单例服务方对象,破坏了单例模式的宗旨。问题还是挺严重的。所以需要进行后面的优化。
延迟实例化:第一版代码优化(线程安全,低效)
LocalCacheManagerV11:第一版优化后
要点提示:
静态getInstance()方法:为了线程安全,这个静态方法增加了synchronized关键字,确保统一时刻,只有一个线程可以访问这个方法。package com.geekarchitect.patterns.singleton.demo01; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * 本地缓存管理器 * * @author 极客架构师@吴念 * @createTime 2022/5/30 */ public class LocalCacheManagerV11 implements ILocalCache { private static final Logger LOG = LoggerFactory.getLogger(LocalCacheManagerV11.class); private static Cache<String, String> guavaCache = null; private static LocalCacheManagerV11 localCacheManagerV11 = null; private LocalCacheManagerV11() { initCache(); } public static synchronized LocalCacheManagerV11 getInstance() { if (null == localCacheManagerV11) { LOG.info("延迟实例化:第一版代码优化(线程安全,低效)"); localCacheManagerV11 = new LocalCacheManagerV11(); } return localCacheManagerV11; } private void initCache() { LOG.info("初始化缓存"); guavaCache = CacheBuilder.newBuilder().build(); } @Override public void put(String key, String value) { guavaCache.put(key, value); } @Override public String get(String key) { return guavaCache.getIfPresent(key); } }
运行结果
头脑风暴
这版代码,与上一版代码,只有一个细微的差别,就是为了线程安全,这个静态getInstance()方法增加了synchronized关键字。解决了多线程的安全问题,不论有多少个线程访问这个方法,同一时刻,都只能有一个线程获取到锁,这样就避免了创建多个对象的风险。但是代码太大了,大到不能承受。
由于我们增加了synchronized关键字,导致调用getInstance()方法,由并发调用,变成了低效率的串行调用,也就是排队,一个接一个调用。在高并发的互联网项目中,是不能容忍的,所以这版代码作废,我们必须继续升级。
延迟实例化:第一版代码再优化(线程安全,双重检测,高效,推荐方案)
LocalCacheManagerV11:第一版二次优化
要点提示:
静态getInstance()方法:为了线程安全,并且高效,使用了双重检测,大家注意,这里面有两个if语句。package com.geekarchitect.patterns.singleton.demo01; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * 本地缓存管理器 * * @author 极客架构师@吴念 * @createTime 2022/5/30 */ public class LocalCacheManagerV11 implements ILocalCache { private static final Logger LOG = LoggerFactory.getLogger(LocalCacheManagerV11.class); private static Cache<String, String> guavaCache = null; private static LocalCacheManagerV11 localCacheManagerV11 = null; private LocalCacheManagerV11() { initCache(); } public static LocalCacheManagerV11 getInstance() { if (null == localCacheManagerV11) { synchronized (LocalCacheManagerV11.class) { if (null == localCacheManagerV11) { localCacheManagerV11 = new LocalCacheManagerV11(); } } } return localCacheManagerV11; } private void initCache() { LOG.info("初始化缓存"); guavaCache = CacheBuilder.newBuilder().build(); } @Override public void put(String key, String value) { guavaCache.put(key, value); } @Override public String get(String key) { return guavaCache.getIfPresent(key); } }
运行结果
头脑风暴
这版代码,对于静态getInstance()方法,使用了双重检测机制,减少了synchronized 加锁的范围,当多个线程中的单例客户方,并发调用这个方法时,如果对象还没有创建,哪个线程先获取到锁,哪个线程执行创建对象的任务。如果对象已经创建,则直接返回即可,效率非常高。
可以说,这版代码,高效,线程安全,简洁,所以是推荐方案。但是在java5的多线程体现中,引入了一个新的锁,ReentrantLock可重入锁,比较流行,非常灵活,所以对于这版代码,我们还可以换一种实现方式,我们继续。
延迟实例化:第二版代码(线程安全,双重检测,可重入锁,高效)
LocalCacheManagerV12:第二版
要点提示:
1,建立一个可重入锁:ReentrantLock
2,基于ReentrantLock,实现了双重检测机制
package com.geekarchitect.patterns.singleton.demo01; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * * @author 极客架构师@吴念 * @createTime 2022/5/30 */ public class LocalCacheManagerV12 implements ILocalCache { private static final Logger LOG = LoggerFactory.getLogger(LocalCacheManagerV12.class); private static Cache<String, String> guavaCache = null; private static LocalCacheManagerV12 localCacheManagerV01 = null; private static final Lock reentrantLock = new ReentrantLock(); private LocalCacheManagerV12() { initCache(); } public static LocalCacheManagerV12 getInstance() { if (null == localCacheManagerV01) { reentrantLock.lock(); LOG.info("第二版代码(线程安全,双重检测,可重入锁,高效)"); try { if (null == localCacheManagerV01) { localCacheManagerV01 = new LocalCacheManagerV12(); } } finally { reentrantLock.unlock(); } } return localCacheManagerV01; } private void initCache() { LOG.info("初始化缓存"); guavaCache = CacheBuilder.newBuilder().build(); } @Override public void put(String key, String value) { guavaCache.put(key, value); } @Override public String get(String key) { return guavaCache.getIfPresent(key); } }
运行结果
头脑风暴
这版代码实现的代码,也满足线程安全和高效,也实现了双重检测机制,甚至在复杂情况下,可以使用ReentrantLock编写出更简洁,更高效,更灵活的多线程代码,这也是ReentrantLock在java5出现,要对synchronized关键字取而代之的一个重要原因。
但是,但是,但是,凡事都有代价,前提就是你必须精通ReentrantLock,如果不精通,编写出的代码就可能存在漏洞。你如果能明白,我为什么要把ReentrantLock的unlock()方法调用,放在finally块中,应该就具备这种资格。
延迟实例化:第三版代码(线程安全,静态内部类)
LocalCacheManagerV13:第三版
要点提示:
1,增加了一个静态内部类:LocalCacheManagerHolder
2,静态内部类中定义了一个静态属性,用于存储单例服务方对象。
package com.geekarchitect.patterns.singleton.demo01; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * @author 极客架构师@吴念 * @createTime 2022/5/30 */ public class LocalCacheManagerV13 implements ILocalCache { private static final Logger LOG = LoggerFactory.getLogger(LocalCacheManagerV13.class); private static Cache<String, String> guavaCache = null; private LocalCacheManagerV13() { initCache(); } public static LocalCacheManagerV13 getInstance() { return LocalCacheManagerHolder.localCacheManagerV13; } private void initCache() { LOG.info("第三版代码(线程安全,静态内部类)"); LOG.info("初始化缓存"); guavaCache = CacheBuilder.newBuilder().build(); } @Override public void put(String key, String value) { guavaCache.put(key, value); } @Override public String get(String key) { return guavaCache.getIfPresent(key); } public static class LocalCacheManagerHolder { private static final LocalCacheManagerV13 localCacheManagerV13 = new LocalCacheManagerV13(); } }
运行结果
头脑风暴
这版代码,从代码复杂度上看,精简了不少,也不用考虑什么多线程问题,它也是线程安全的。这些都有赖于java的静态内部类,里面的静态属性初始化机制。也就是多线程问题我们不用考虑了,全部交给JVM来处理。但是静态内部类,在常见的项目中,大部分人并不了解它的机制,精通的人更少,所以比较少见。这里仅供大家参考。我是不建议使用大家都不太熟悉的,奇奇怪怪的语法,在团队开发中,有百害而无一利,得不偿失。
单例模式的延迟实例化方式,我们全部讲解完毕。下面我们看另外一个方式,提前实例化。
单例模式需要深入思考的点
提前实例化和延迟实例化,该选哪个?
如果搞不清楚,就选提前实例化,代码简单,还线程安全。如果对代码质量精益求精,则在单例服务方有可能不会被使用的业务场景,考虑使用延迟实例化,降低资源浪费的概率。
单例模式通用代码
UML类图
单例模式-提前实例化
package com.geekarchitect.patterns.singleton.demo03; import com.geekarchitect.patterns.singleton.demo02.LocalCacheManagerV22; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * 单例模式:提前实例化 * * @author 极客架构师@吴念 * @createTime 2022/6/1 */ public class EarlyInstance { private static final Logger LOG = LoggerFactory.getLogger(EarlyInstance.class); private static final EarlyInstance EARLY_INSTANCE = new EarlyInstance(); private EarlyInstance() { } public static EarlyInstance getInstance() { return EARLY_INSTANCE; } public void doService(){ LOG.info("单例模式:提前实例化 "); } }
单例模式-延迟实例化
package com.geekarchitect.patterns.singleton.demo03; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * 单例模式:延迟实例化 * * @author 极客架构师@吴念 * @createTime 2022/6/1 */ public class LazyInstance { private static final Logger LOG = LoggerFactory.getLogger(LazyInstance.class); private static LazyInstance LAZY_INSTANCE; private LazyInstance() { } public static LazyInstance getInstance() { if (null == LAZY_INSTANCE) { synchronized (LazyInstance.class) { if (null == LAZY_INSTANCE) { LAZY_INSTANCE = new LazyInstance(); } } } return LAZY_INSTANCE; } public void doService(){ LOG.info("单例模式:延迟实例化"); } }
结论
综上所述,根据REIS分析模型,对单例模式进行分析,单例模式包含两种角色,单例服务方角色和单例客户方角色。它的宗旨是,单例服务方角色,通过私有化的构造函数,静态的类属性以及静态的getInstance()方法等措施,确保所有单例客户方,都只能获取到它的唯一一个对象。该模式的研究重点是对象实例化的时机问题,可以分为两种实例化方式,提前实例化和延迟实例化。
本期我们就分享到这里,关注我,我将持续分享更多架构师的相关文章和视频,我们下期见。


还没有评论,来说两句吧...