大家好,欢迎关注极客架构师,极客架构师,专注架构师成长,我是码农老吴。
本期是《架构师基本功之设计模式》的第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()方法等措施,确保所有单例客户方,都只能获取到它的唯一一个对象。该模式的研究重点是对象实例化的时机问题,可以分为两种实例化方式,提前实例化和延迟实例化。
本期我们就分享到这里,关注我,我将持续分享更多架构师的相关文章和视频,我们下期见。
如若转载,请注明出处:https://www.sumedu.com/faq/96337.html