当前位置: 首页 > news >正文

多线程代码案例-1 单例模式

单例模式

单例模式是开发中常见的设计模式。

设计模式,是我们在编写代码时候的一种软性的规定,也就是说,我们遵守了设计模式,代码的下限就有了一定的保证。设计模式有很多种,在不同的语言中,也有不同的设计模式,设计模式也可以被认为是对编程语言语法的补充

单例即单个实例(对象),某个类在一个进程中,只应该创建出一个实例(原则上不应该创建出多个实例),使用单例模式,可以对我们的代码进行一个更为严格的校验和检查。

举个例子:有时候,代码中需要管理/持有大量的数据,此时有一个对象就可以了。比如:我需要一个对象管理10G的数据,如果我们不小心创建出多个对象,内存空间就会成倍地增长。

如何保证只有唯一的对象呢?我们可以选择“君子之约地方式”,写一个文档,文档上约定,每个接手维护代码的程序员,都不能对这个类创建多个实例(很显然,这种约定并不靠谱)我们期望让机器(编译器)能够对代码中的指定类,对创建的实例个数进行检验。如果发现创建出了多个实例,就直接编译报错,但是Java语法中本身没有办法直接约定某个对象能创建出几个实例,那么就需要程序员使用一些技巧来实现这样的效果。

实现单例模式的方式有很多种,这里介绍两种实现方式:饿汉模式和懒汉模式。

1 饿汉模式

代码如下:

//饿汉模式
//期望这个类只能有唯一的实例(一个进程中)
class Singleton{private static Singleton instance = new Singleton();//在这个类被加载时,就会初始化这个静态成员,实例创建的时机非常早——饿汉public static Singleton getInstance(){//其他代码想要使用这个类的实例就需要通过这个方法进行获取,// 不应该在其他代码中重新new这个对象而是使用这个方法获取这个现有的对象return instance;}private Singleton(){//其他代码就没法new了}
}

在这个类中,我们创建出了唯一的对象,被static修饰,说明这个变量是类变量,(由类对象所拥有(每个类的类对象只存在一个),在类加载的时候,它就已经被初始化了)

而将构造方法设为私有,就使得只能在当前类里面创建对象了,其他位置就不能再创建对象了,因此这个instance指向的对象就是唯一的对象。

其他代码要想使用这个类的实例,就需要通过这个getInstance()方法获取这个对象,而无法在其他代码中new一个对象。

上述代码,称为”饿汉模式“,是单例模式中的一种简单的写法,”饿“形容”非常迫切“,实例在类加载的时候就创建了,创建的时机非常早,相当于程序一启动,实例就创建了。 

但是,上面的代码,面对反射,是无能为力的,也就是说,仍然可以通过反射来创建对象,但反射是属于非常规的编程手段,代码中随意使用反射是非常糟糕的。

2 懒汉模式

”懒“这个词,并不是贬义词,而是褒义词。社会能进步,科技能发展,生产效率提高,有很大部分原因都是因为懒。

举个生活中的例子(不考虑卫生):

假如我每次吃完饭就洗碗,那我每次就需要洗全部的碗;但是如果我每次吃完饭把碗放着,等到下次吃饭的时候再洗,此时,如果我只要用到两个碗,那我就只需要洗两个碗就行了,很明显洗两个碗要比洗全部碗更加高效。

在计算机中,”懒“的思想就非常有意思,它通常代表着更加高效

比如有一个非常大的文件(10GB),使用编辑器打开这个文件,如果是按照”饿汉“的方式 ,编辑器就会先把这10GB的数据都加载到内存中,然后再进行统一的展示。(但是加载了这么多数据,用户还是需要一点一点地看,没法一下子看完这么多)

如果是按照”懒汉“地方式,编辑器就会只读取一小部分数据(比如只读取10KB),把这10KB先展示出来,然后随着用户进行翻页之类的操作,再继续展示后面的数据。

加载10GB的时间会很长,但是加载10KB却只是一瞬间的事情……

懒汉模式,区别于饿汉模式,创建实例的时机不一样了,创建实例的时机会更晚,一直到第一次使用getInstance方法时才会创建实例。

代码如下(注意:这是一个不完整的代码,因为还有一些线程安全问题需要解决~~):

//懒汉的方式实现单例模式class SingletonLazy{private static SingletonLazy instance = null;public static  SingletonLazy getInstance(){//饿汉模式是在类加载的时候就创建实例了,懒汉则会晚很多,且如果程序用不到这个方法就会省下了if (instance == null) {//如果首次调用就创建实例instance = new SingletonLazy();}}}//不是则返回之前创建的引用return instance;}private SingletonLazy(){}
}

第一行代码中仍然是先创建一个引用,但是这个引用不指向任何的对象。如果是首次调用getInstance方法,就会进入if条件,创建出对象并且让当前引用指向该对象。如果是后续调用getInstance方法,由于当前的instance已经不是null了,就会返回我们之前创建的引用了。

这样设定,仍然可以保证,该类的实例是唯一一个,与此同时,创建实例的时机就不再是程序驱动了,而是当第一次调用getInstance的时候,才会创建。。

而第一次调用getInstance这个操作的执行时机就不确定了,要看程序的实际需求,大概率会比饿汉这种方式要晚一些,甚至有可能整个程序压根用不到这个方法,也就把创建的操作给省下了。

有的程序,可能是根据一定的条件,来决定是否要进行某个操作,进一步来决定是否要创建实例。 

3 单例模式与线程安全

上面我们介绍的关于单例模式只是一个开始,接下来才是我们多线程的真正关键问题。即:上述我们编写的饿汉模式和懒汉模式,是否是线程安全的?

饿汉模式:

//饿汉模式
//期望这个类只能有唯一的实例(一个进程中)
class Singleton{private static Singleton instance = new Singleton();//在这个类被加载时,就会初始化这个静态成员,实例创建的时机非常早——饿汉public static Singleton getInstance(){//其他代码想要使用这个类的实例就需要通过这个方法进行获取,// 不应该在其他代码中重新new这个对象而是使用这个方法获取这个现有的对象return instance;}private Singleton(){//其他代码就没法new了}
}

对于饿汉模式来说,getInstance直接返回instance这个实例,这个操作,本质上就是一个的操作(多个线程同时读取同一变量,是不会产生线程安全问题的)。因此,在多线程下,它是线程安全的。

懒汉模式 :

//懒汉的方式实现单例模式class SingletonLazy{private static SingletonLazy instance = null;public static  SingletonLazy getInstance(){//饿汉模式是在类加载的时候就创建实例了,懒汉则会晚很多,且如果程序用不到这个方法就会省下了if (instance == null) {//如果首次调用就创建实例instance = new SingletonLazy();}//不是则返回之前创建的引用return instance;}private SingletonLazy(){}
}

再看懒汉模式,在懒汉模式中,代码中有的操作(return instance),又有的操作(instance = new SingletonLazy())。 很明显,这是一个有线程安全问题的代码!!!

问题1:线程安全问题

因为多线程之间是随机调度,抢占是执行的,如果t1和 t2 按照下列的顺序执行代码,就会出现问题。

如果是t1和t2按照上述情况操作,就会导致实例被new了两次,这就不是单例模式了,就会出现bug了!!!

那如何解决当前的代码bug,使它变为一个线程安全的代码呢?

加锁~~

知道要加锁了?那大家不妨想想:如果我把锁像如下代码这样加下去,是否线程就安全了呢?

class SingletonLazy{private static SingletonLazy instance = null;Object locker = new Object;public static  SingletonLazy getInstance(){//饿汉模式是在类加载的时候就创建实例了,懒汉则会晚很多,且如果程序用不到这个方法就会省下了if (instance == null) {//如果首次调用就创建实例sychronized(locker){instance = new SingletonLazy();}}//不是则返回之前创建的引用return instance;}private SingletonLazy(){}
}

答案很显然:不行!!!因为如上述代码加锁仍然会发生刚才那样的线程不安全的情况。

所以这里如果想要代码正确执行,需要把if和new两个操作,打包成一个原子的操作(即加锁加在if语句的外面)。 

class SingletonLazy{private static SingletonLazy instance = null;Object locker = new Object;public static  SingletonLazy getInstance(){//饿汉模式是在类加载的时候就创建实例了,懒汉则会晚很多,且如果程序用不到这个方法就会省下了synchronized(locker){    if (instance == null) {//如果首次调用就创建实例instance = new SingletonLazy();}}  //不是则返回之前创建的引用return instance;}private SingletonLazy(){}
}

 

此时因为t1拿到了锁,t2进入阻塞,等t1执行完毕后(创建完对象后),t2进行判断,此时因为t1已经创建好了对象,所以t2就只能返回当前对象的引用了。 

多线程的代码是非常复杂的,代码稍微变化一点,结论就可能截然不同。千万不能认为,代码中加了锁就一定线程安全,不加锁就一定线程不安全,具体问题要具体分析,要分析这个代码在各种调度执行顺序下不同的情况,确保每种情况都不会出现bug!!!

 问题2:效率问题

上述代码还存在的另一个问题是效率问题:试想一下,当你创建完这个单例对象,你每次获取这个单例对象时(是读的操作,并不会有线程问题),每次都要去加锁、解锁,然后才能返回这个对象。(注意:加锁、解锁耗费的空间和时间都是很大的)。

所以为了优化上面的代码,我们可以再加上一层if,如果instance为null(需要执行写操作),考虑到线程安全问题,就需要加锁;如果instance不为null了,就不需要加锁了。

class SingletonLazy{private static SingletonLazy instance = null;Object locker = new Object;public static  SingletonLazy getInstance(){//饿汉模式是在类加载的时候就创建实例了,懒汉则会晚很多,且如果程序用不到这个方法就会省下了if(instance == null){synchronized(locker){    if (instance == null) {//如果首次调用就创建实例instance = new SingletonLazy();}}}    //不是则返回之前创建的引用return instance;}private SingletonLazy(){}
}

上面的代码,有两重完全相同if判断条件,但是他们的作用是完全不同的:

第一个if是判断是否需要加锁,第二个if是判断是否要创建对象!!!

巧合的是,两个if条件相同,但是他们的作用是完全不同的,这样就实现了双重校验锁。在以后的学习中,还可能出现两个if条件是相反的情况。

问题3:指令重排序问题

这个代码还有一点问题需要解决:我们之前在线程安全的原因中讲过的:指令重排序问题就在懒汉模式上出现了~~

指令重排序,也是编译器优化的一种方式。编译器会在保证逻辑不变的前提下,为了提高程序的效率,调整原有代码的执行顺序。

再举个生活中的例子:

我妈让我去超市买东西:西红柿、鸡蛋、黄瓜、茄子。

超市摊位分布图如下:

如果我按我妈给的顺序,那就会走出这样的路线: 

上述方案虽然也能完成我妈给的任务,但如果我对超市已经足够熟悉了,我就能够在保证逻辑不变

的情况下(买到4种菜),调整原有买菜的执行顺序,提高买菜效率: 

返回到代码中:

   instance = new SingletonLazy();

 上面这行代码,可以拆分为三个步骤:

1、申请一段内存空间。

2、调用构造方法,创建出当前实例。

3、把这个内存地址赋给instance这个引用。

上述代码可以按1、2、3这个顺序来执行,但是编译器也可能会优化成1、3、2这个顺序执行。这两种顺序在单线程下都是能够完成任务的。

1就相当于买了个房子

2相当于装修房子

3相当于拿到了房子的钥匙

通过1、2、3得到的房子,拿到的房子已经是装修好的,称为“精装房”;通过1、3、2得到的房子,拿到的房子需要自己装修,称为“毛坯房”,我们买房子时,上面的两种情况都可能发生。

但是,如果在多线程环境下,指令重排序就会引入新问题了。

上述代码中,由于 t1 线程执行完 1 3 步骤(申请一段内存空间,把内存空间的地址赋给引用变量,但并没有进行 2 调用构造方法的操作,会导致 instance指向的是一个未被初始化的对象)之后调度走,此时 instance 指向的是一个非 null 的,但是是未初始化的对象,此时 t2 线程判定 instance == null 不成立,就会直接 return,如果 t2 继续使用 instance 里面的属性或者方法,就会出现问题,引起代码的逻辑出现问题。 

那么我们应该如何解决当前问题呢?

volatile关键字

之前讲过volatile有两个功能:

1、保证内存可见性:每次访问变量都必须要重新读取内存,而不会优化为读寄存器/缓存。

2、禁止指令重排序:针对被volatile修饰的变量的读写操作的相关指令,是不能被重排序的。

懒汉模式的完整代码:

//经典面试题!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
package Thread;
//懒汉的方式实现单例模式
//线程不安全,它在多线程环境下可能会创建多个实例
class SingletonLazy{//这个引用指向唯一实例,这个引用先初始化为null,而不是立即创建实例
private volatile static SingletonLazy instance = null;//针对这个变量的读写操作就不能重排序了
private static Object locker;
//第一次if判定是否要加锁,第二次if判定是否要创建对象//双重校验锁public static  SingletonLazy getInstance(){//饿汉模式是在类加载的时候就创建实例了,懒汉则会晚很多,且如果程序用不到这个方法就会省下了//加锁效率不高,且容易导致阻塞,所以再加一个判断提高效率if(instance ==null) {//判断是否为空,为空再加锁//不为空,说明是后续的调用就无需加锁了synchronized (locker) {if (instance == null) {//如果首次调用就创建实例instance = new SingletonLazy();}}}//不是则返回之前创建的引用return instance;}private SingletonLazy(){}
}
http://www.xdnf.cn/news/421705.html

相关文章:

  • 在spark中配置历史服务器
  • 【C++】深入理解 unordered 容器、布隆过滤器与分布式一致性哈希
  • 拓扑排序详解
  • H5S 视频监控AWS S3 对象存储
  • BGP实验练习2
  • Github 2025-05-13 Python开源项目日报 Top10
  • 从零开始:使用 Vue-ECharts 实现数据可视化图表功能
  • 详解Windows(十一)——网络连接设置
  • 解锁ozon运营新路径:自养号测评技术如何实现降本增效
  • CSS结构性伪类、UI伪类与动态伪类全解析:从文档结构到交互状态的精准选择
  • 【Flask全栈开发指南】从零构建企业级Web应用
  • Vue3+uniapp 封装axios
  • 《猜拳游戏》
  • 深入学习Zookeeper的知识体系
  • 软件测试服务公司分享:国产化适配测试的重要性和关键要素
  • 如何在 CentOS 7 虚拟机上配置静态 IP 地址并保持重启后 SSH 连接
  • ios remote debut proxy 怎么开启手机端调试和inspect
  • C++ string数据查找、string数据替换、string子串获取
  • Rollup入门与进阶:为现代Web应用构建超小的打包文件
  • 【23种设计模式】分类结构有哪些?
  • Java——集合基础
  • OpenCV中的光流估计方法详解
  • 前端面试每日三题 - Day 33
  • 深入理解BLP安全模型:信息安全中的“守密者”
  • win部署Jenkins 自动化部署发布后端项目
  • 文件操作: File 类的用法和 InputStream, OutputStream 的用法
  • 构建媲美 ChatGPT 的 AI 交互界面—OpenWebUI
  • 大模型分布式光伏功率预测实现详解
  • Linux—进度条实现
  • 开源网络地图可视化第六章学习指南