大家好呀,这里是纵横,今天,我们继续看电影学编程。我们继续结合《红海行动》里的场景学习高并发编程。
上一节我们从整体上介绍了红海行动中与多线程相关的场景,从今天开始,我们开始一边从头看电影,一边用代码模拟其中的有趣场景。
首先,电影开场之后,我们可以看到一搜孤立的货船广东号,既然唯一,那我们能想到什么呢?单例模式!
继续看电影现在画面出现了一个绑匪,独眼龙,又是唯一,单例模式。
当然,这里强行与单例模式绑定有点牵强。不过后宫争宠等等电视剧大家看了不少,也更像单例模式。最经典的是小品《甄嬛后传》里的梗(源自宫斗剧宠妃人设)(宋小宝):“自打我进宫以来,后宫佳丽三千,皇上就偏偏宠我一人,我劝皇上雨露均沾,可皇上非是不听,就宠我、就宠我”。此外宫斗剧正剧里也有贴合表述,如《甄嬛传》中旁人评价华妃:“翊坤宫的恩宠独一份,连皇后都动摇不得”;或皇上对宠妃说:“这后宫,朕眼里心里,唯有你一人”,
既然先出现了单例,那我们就先简单看一下单例模式吧。
在看单例模式之前呢 ,我们先思考一个问题,我们平时写的java生成的字节码 就像图纸一样,计算机在需要的时候用这个字节码来创建对象, 理论上,我们可以创建很多对象。但是有时候,我们要限制一个类只能创建出一个对象。在现实中,单例其实也是很常见的,比如一个公司只有一个老大,一个国家只能有一个最高领导人等等。这么做有什么好处呢?就是防止大脑分裂身体不知道该干嘛。比如米国有两个政党,轮番上台。形势好的时候还没什么影响, 而到了形势不好的时候,就会相互拆台、相互攻击、政令不一,谁都不能执行长期有利于国家的政策。而下面的人就会无所适从,不知道该干什么,因此单例是避免一个内耗的好方法。假如你有两个wife,那可能明天都要为解决他们的宫斗而浪费时间,所以呢,有一个就行了。
在计算机里,单例也是避免线程冲突的有效方法。比如我们保存数据库连接信息的对象、连接其他服务的对象,这些是不是最好就一个?而且最好是启动的时候就加载,之后哪个代码需要直接调就好了。而且修改的话,因为只有一个对象,我们修改也简单,是不是?
所以对于这些信息,我们设计成单例的将大大减小后期的管理负担。
再比如,在疫情期间,有个新闻大家应该都映像深刻:
当时看到这个新闻时候,我一直有个疑惑:为什么要把两个医院都交给一个公司来做,分一个给其他的不好吗?
后来结合单例模式,我理解了,为什么呢?因为工期非常短,要求特别高,正值春节和疫情的限制,资源又非常紧张,此时必须两个医院统筹安排,以将效率最大化。比如一个推土机,上午在火神山推完土,下午就要准时到雷神山推土。而假如是两个单位分别负责,可能相互之间都不知道几个推土机能用,其沟通、管理成本将非常高。而如果一个单位统一管理,就好很多。这也是单例的最大作用。
【2】单例模式的几种方式
那如何将一个类限制为只能创建一个对象呢?这个实现有很多种方法,简单来说有饿汉式和懒汉式两大类。
所谓饿汉式就是类加载期间就创建出对象,主要就是JVM启动的时候就创建出来,代码如下:
public class Singleton_01 { //1. 私有构造方法 privateSingleton_01(){ } //2. 在本类中创建私有静态的全局对象 private static Singleton_01 instance = new Singleton_01(); //3. 提供一个全局访问点,供外部获取单例对象 publicstatic Singleton_01 getInstance(){ return instance; }}
特点: 不支持延迟加载实例(懒加载) , 此中方式类加载比较慢,但是获取实例对象比较快
问题: 该对象足够大的话,而一直没有使用就会造成内存的浪费。
而懒汉式实现方式就多了,我们这里简单看一下,请自己学习,如果学不懂,请私信老师答疑解惑:
【1】懒汉式(线程不安全)
此种方式的单例实现了懒加载,只有调用getInstance方法时 才创建对象.但是如果是多线程情况,会出现线程安全问题.
public class Singleton_02 { //1. 私有构造方法 privateSingleton_02(){ } //2. 在本类中创建私有静态的全局对象 private static Singleton_02 instance; //3. 通过判断对象是否被初始化,来选择是否创建对象 publicstatic Singleton_02 getInstance(){ if(instance == null){ instance = new Singleton_02(); } return instance; }}
解释:假设在单例类被实例化之前,有两个线程同时在获取单例对象,线程A在执行完if (instance == null) 后,线程调度机制将 CPU 资源分配给线程B,此时线程B在执行 if (instance == null)时也发现单例类还没有被实例化,这样就会导致单例类被实例化两次。为了防止这种情况发生,需要对 getInstance() 方法同步处理。
【2】懒汉式(线程安全)
原理: 使用同步锁 synchronized锁住 创建单例的方法 ,防止多个线程同时调用,从而避免造成单例被多次创建
即,getInstance()方法块只能运行在1个线程中
若该段代码已在1个线程中运行,另外1个线程试图运行该块代码,则 会被阻塞而一直等待
而在这个线程安全的方法里我们实现了单例的创建,保证了多线程模式下 单例对象的唯一性
public class Singleton_03 { //1. 私有构造方法 privateSingleton_03(){ } //2. 在本类中创建私有静态的全局对象 private static Singleton_03 instance; //3. 通过添加synchronize,保证多线程模式下的单例对象的唯一性 publicstatic synchronized Singleton_03 getInstance(){ if(instance == null){ instance = new Singleton_03(); } return instance; }}
懒汉式的缺点也很明显,我们给 getInstance() 这个方法加了一把大锁(synchronzed),导致这个函数的并发度很低。量化一下的话,并发度是 1,也就相当于串行操作了。而这个函数是在单例使用期间,一直会被调用。如果这个单例类偶尔会被用到,那这种实现方式还可以接受。但是,如果频繁地用到,那频繁加锁、释放锁及并发度低等问题,会导致性能瓶颈,这种实现方式就不可取了。
【3】双重校验
饿汉式不支持延迟加载,懒汉式有性能问题,不支持高并发。那我们再来看一种既支持延迟加载、又支持高并发的单例实现方式,也就是双重检测实现方式。
实现步骤:
在声明变量时使用了 volatile 关键字,其作用有两个:
保证变量的可见性:当一个被volatile关键字修饰的变量被一个线程修改的时候,其他线程可以立刻得到修改之后的结果。
屏蔽指令重排序:指令重排序是编译器和处理器为了高效对程序进行优化的手段,它只能保证程序执行的结果是正确的,但是无法保证程序的操作顺序与代码顺序一致。这在单线程中不会构成问题,但是在多线程中就会出现问题。
将同步方法改为同步代码块. 在同步代码块中使用二次检查,以保证其不被重复实例化 同时在调用getInstance()方法时不进行同步锁,效率高。
/** * 单例模式-双重校验 **/public class Singleton_04 { //使用 volatile保证变量的可见性 private volatile static Singleton_04 instance = null; privateSingleton_04(){ } //对外提供静态方法获取对象 publicstatic Singleton_04 getInstance(){ //第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例 if(instance == null){ synchronized (Singleton_04.class){ //抢到锁之后再次进行判断是否为null if(instance == null){ instance = new Singleton_04(); } } } return instance; }}
在双重检查锁模式中为什么需要使用 volatile 关键字?
在java内存模型中,volatile 关键字作用可以是保证可见性或者禁止指令重排。这里是因为 singleton = new Singleton() ,它并非是一个原子操作,事实上,在 JVM 中上述语句至少做了以下这 3 件事:
这里需要留意一下 1-2-3 的顺序,因为存在指令重排序的优化,也就是说第 2 步和第 3 步的顺序是不能保证的,最终的执行顺序,可能是 1-2-3,也有可能是 1-3-2。
如果是 1-3-2,那么在第 3 步执行完以后,singleton 就不是 null 了,可是这时第 2 步并没有执行,singleton 对象未完成初始化,它的属性的值可能不是我们所预期的值。假设此时线程 2 进入 getInstance 方法,由于 singleton 已经不是 null 了,所以会通过第一重检查并直接返回,但其实这时的 singleton 并没有完成初始化,所以使用这个实例的时候会报错.
详细流程如下图所示:
线程 1 首先执行新建实例的第一步,也就是分配单例对象的内存空间,由于线程 1 被重排序,所以执行了新建实例的第三步,也就是把 singleton 指向之前分配出来的内存地址,在这第三步执行之后,singleton 对象便不再是 null。
这时线程 2 进入 getInstance 方法,判断 singleton 对象不是 null,紧接着线程 2 就返回 singleton 对象并使用,由于没有初始化,所以报错了。最后,线程 1 “姗姗来迟”,才开始执行新建实例的第二步——初始化对象,可是这时的初始化已经晚了,因为前面已经报错了。
使用了 volatile 之后,相当于是表明了该字段的更新可能是在其他线程中发生的,因此应确保在读取另一个线程写入的值时,可以顺利执行接下来所需的操作。在 JDK 5 以及后续版本所使用的 JMM 中,在使用了 volatile 后,会一定程度禁止相关语句的重排序,从而避免了上述由于重排序所导致的读取到不完整对象的问题的发生。
此外还有通过静态内部类和枚举等实现方式,其代码略微复杂,我们这里就不展开了,不过呢,这个问题很重要,如果不理解,请私信咨询。
实现单例 模式有多种方式,具体采用哪种取决于不同实现的特征,总结如下:
【3】单例模式在源码中的应用举例
既然单例模式这么重要,那在源码里,特别是常见开源框架有什么应用呢?以下是一些在知名Java框架源码中使用双重检测锁(Double-Checked Locking)实现单例模式的经典案例,这些案例都来自当前主流版本的实际源码实现。
1.DCL 在JVM创建扩展类加载器中的应用
我们知道JVM 里有引导类加载器、扩展类加载器和系统类加载器,这几个类加载器都要求唯一的,那JVM如何保证扩展类加载器唯一的呢?就是DCL:
static class ExtClassLoader extends URLClassLoader { private static volatile Launcher.ExtClassLoader instance; public static Launcher.ExtClassLoader getExtClassLoader() throws IOException { if (instance == null) { Class var0 = Launcher.ExtClassLoader.class; synchronized(Launcher.ExtClassLoader.class) { if (instance == null) { instance = createExtClassLoader(); } } } return instance; }
在面试的时候,如果聊设计模式,我们可以通过上面的例子引到JVM上。如果聊JVM,则可以通过这个例子引到设计模式上,所以只要我们都掌握好了,就有很大的把握来引导面试官。
除此之外这些内容,我们没必要去背,了解即可,如果想面试的时候更有杀伤力,可以说“双重检测锁单例模式在开源框架中大量应用,例如Tomcat加载配置、加载JSP、加载日志、处理jsp字节流等场景大量使用就行了。”
2. Apache Tomcat (9.x)
不过具体使用哪种,还需要考虑作者的倾向和喜好,例如在Tomcat中就大量使用了双重检测锁结构的代码,当然很多并不是严格意义的单例模式:
例如获取配置的实现如下:
public ServerAuthConfig getServerAuthConfig(String layer, String appContext, CallbackHandler handler) throws AuthException { ServerAuthConfig serverAuthConfig = this.serverAuthConfig; if (serverAuthConfig == null) { synchronized (this) { if (this.serverAuthConfig == null) { this.serverAuthConfig = createServerAuthConfig(layer, appContext, handler, properties); } serverAuthConfig = this.serverAuthConfig; } } return serverAuthConfig;}
或者构造日志的代码:
private Log getLog() { if (log == null) { synchronized (this) { if (log == null) { log = LogFactory.getLog(ReplicatedMap.class); } } } return log;}
Dubbo (3.x)
文件路径:org/apache/dubbo/rpc/model/ApplicationModel.java类名:ApplicationModel方法名:getApplication(
private static volatile ApplicationModel APPLICATION_MODEL;publicstatic ApplicationModel getApplication() { if (APPLICATION_MODEL == null) { synchronized (ApplicationModel.class) { if (APPLICATION_MODEL == null) { // 双重检测 APPLICATION_MODEL = new ApplicationModel(applicationName); } } } return APPLICATION_MODEL;}