【Java多线程案例】单例模式

news2024/12/23 17:15:11

本期讲解单例模式的饿汉模式与懒汉模式,以及如何解决懒汉模式造成线程的不安全问题。

目录

什么是单例模式?

1. 饿汉模式

2. 懒汉模式

2.1 懒汉模式单线程版

2.2 懒汉模式多线程版

3.  解决懒汉模式不安全问题

3.1 保证原子性

3.2 防止指令重排序


什么是单例模式?

首先,单例模式是一种设计模式。何为设计模式,设计模式类似于固定的套路。例如考驾照的科目二项目,教练会总结出一些点位,因此我们按照这些点位去练习然后考试就能很顺畅的通过。在 Java 中也是如此,常见的就是开发中前辈设计好的一些案例,我们直接拿来用即可。

单例模式是在进程中有且仅有一份实例的模式,所以我们称之为单例。此外单例模式分为饿汉模式懒汉模式

通过上图,我们可以看到。thread1 - thread3 都共用 Singleton 这个实例,这样的一个模式就是单例模式。


1. 饿汉模式

看到饿汉二字,我们就会想到这是一种饥渴的状态,有一种一看到饭就冲上去吃的感觉。因此,饿汉模式它是一种类加载时就创建对象的一种模式,如下代码:

//自定义类singleton
class Singleton {
    //创建一个对象
    private static Singleton instance = new Singleton();
    //提供一个获取instance的方法
    private static Singleton getInstance(){
        return instance;
    }
}

当以上代码中的自定义类 singleton 被加载后,就会创建一个 instance的对象。这时候我们就可以通过一个获取 instance 对象的方法 getInstance 来使用这个实例。由于 singleton 类中的所有成员变量与成员方法都是被 private 修饰,因此达到了封装效果也体现出了单例模式的唯一性

饿汉模式强调一个饥渴,类一被加载就创建了一个对象。它不存在线程安全问题,当多个线程调用这个饿汉模式时得到的都是同一个实例,并不重新创建实例。

以上的饿汉模式,设计得还是有问题的。如果我们新建了一个实例,这样就不能保证饿汉模式是一个单例模式,如以下代码:

public static void main(String[] args) {
        //s1和s2都是同一个实例
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        //s3新建了一个实例
        Singleton s3 = new Singleton();
    }

因此,我们必须保证在 Singleton 类不能被实例化,这时我们可以在 Singleton类 中提供一个被 private 修饰的构造方法,这样无论如何 Singleton 类都不能被 new 了。如下行代码:

private Singleton(){};

2. 懒汉模式

饿汉模式体现了一种饥渴,懒汉模式给人感觉就是一种懒散的状态,一碗饭在面前爱吃不吃的感觉。因此,懒汉模式在创建实例时并不在类加载时创建对象,而是什么时候需要创建对象了就去创建,不需要则不创建

举个例子,在家里面,吃午餐用了五个盘子,由于很懒没有及时的去洗。到了晚上,炒菜发现没盘子可用了才洗个盘子用来盛菜。剩余的四个盘子还是不洗,至于臭了还是烂了并不在意。这就是体现出懒汉模式中的“懒”状态。


2.1 懒汉模式单线程版

通过上方例子的讲解,我们可以了解到。懒汉模式在使用某个对象时,得判断该是否实例化。如果实例化过了就不创建直接返回该实例,没有则创建后返回该实例。如下流程图:

案例:通过懒汉模式创建一个自定义类 SingletonLazy ,并在 main 方法中创建两个 SingletonLazy 类的引用 s1、s2,使得 s1 等于 s2。因此,我们可以写出以下代码:

class SingletonLazy {
    //创建一个SingletonLazy的实例为空
    private static SingletonLazy instance = null;
    //获取该实例的方法
    public static SingletonLazy getInstance(){
        if (instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }
    //提供一个private修饰的构造方法,保证唯一性
    private SingletonLazy(){};
}
public class ThreadDemo2 {
    public static void main(String[] args) {
        //s1和s2是同一个实例
        SingletonLazy s1 = SingletonLazy.getInstance();
        SingletonLazy s2 = SingletonLazy.getInstance();
        System.out.println(s1 == s2);
    }
}

运行后打印:

以上代码,与饿汉模式相比来说更符合与现实开发。当然,上述代码是单线程版的饿汉模式。因此是比较安全的,但是把以上代码应用到多线程情况下就会造成线程不安全问题。


2.2 懒汉模式多线程版

首先,我们来看下上文中创建的懒汉模式的 SingletonLazy 类的代码。

class SingletonLazy {
    //创建一个SingletonLazy的实例为空
    private static SingletonLazy instance = null;
    //获取该实例的方法
    public static SingletonLazy getInstance(){
        if (instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }
    //提供一个private修饰的构造方法,保证唯一性
    private SingletonLazy(){};
}

在多线程的学习中,我们以及知道了造成线程不安全有线程抢占资源这个概念,其原因就是多个线程在执行过程中进行读和写操作也就是修改操作,导致的不安全问题。

多线程下,懒汉模式会导致创建多个实例,因此不能保证实例的唯一性。假如有多个线程进行调用了 getInstance 方法。线程1在执行同时,由于运行速度过快,线程2也开始执行了,导致最后创建了两个实例。这样就不叫单例模式了。


3.  解决懒汉模式不安全问题


3.1 保证原子性

在上方创建的懒汉模式中的 if 语句 和 new 操作,是不具备原子性的。其原因为在多个线程调用 getIstance 这个方法。


上文中设计的懒汉模式预期的效果为:当多个线程调用 getInstance 方法后。第一个调用 getInstance 的线程会进行 new 操作创建一个 instance 实例,其他线程调用 getInstance 方法后发现 instance 不为 null 则不进行 new 操作。

但由于线程的抢占式执行,导致第一个调用 getIstance 的线程执行到第一步后,其他线程抢占执行了并调用了 getIstance 方法,这个时候两个线程 if 语句都判断 instance 等于 null 。这时候就创建了两个 instance 对象。

这样就导致 if 语句 和 new 操作就不具备原子性(不能完整的执行)。因此,我们可以使用 synchronized 关键字来加锁,使得这两个操作具备原子性。如下代码所示:

//获取该实例的方法
    public static SingletonLazy getInstance(){
        //给if语句和new操作加锁
        synchronized (Singleton.class) {
            if (instance == null) {
                instance = new SingletonLazy();
            }
        }
        return instance;
    }

当然,以上代码加上了锁虽然保证了 if 语句和 new 操作具备了原子性,但还不算是最优的写法。我们可以想象一下,每个线程调用 getInstance 这个方法时候,都会进行锁的竞争这样就会阻塞等待,这样的时间效率是非常低的。

因此,我们可以使用双重 if 语句来减少阻塞等待。如下代码:

public static SingletonLazy getInstance(){
        if (instance == null) {
            //给if语句和new操作加锁
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }

以上的代码中两条 if 语句里面的条件是一样的,但其初心不同。第一条 if 是为了判断是否有 instance 这个实例不让其他线程进入锁的竞争,第二条 if 语句是在锁下进行判断的创建唯一一个实例。

当然,可能多个线程抢占并进入到了第一条 if 语句,但第一个进入锁的线程完成了创建实例任务后,其他线程进入锁后 if 判断的实例不为空也就不会再多创建实例了。

这样的设计才是懒汉模式的标准写法,保证了实例的唯一性。但有一极端的情况,指令被重排序了,具体请看下方讲解。


3.2 防止指令重排序

有一种极端的情况,两个线程同时调用了 getInstance 方法,都进入了第一条 if 语句里面。线程1进入了锁(synchronized)的范围,但由于指令重排序导致 new 这个操作与原本执行顺序不一致。这时候,线程2进入了锁的范围,发现 instance 实例已被创建,则返回 instance 实例。

这样就会导致线程2调用的构造方法是虚无的、不知道是哪里的,造成了线程的不安全。因此,我们在初始化 instance 实例时加上 volatile 关键字,使得指令能够按照顺序进行。

volatile private static SingletonLazy instance = null;

综合起来,创建一个懒汉模式的代码如下所示:

class SingletonLazy {
    //创建一个SingletonLazy的实例为空,volatile修饰保证指令顺序执行
    volatile private static SingletonLazy instance = null;
    //获取该实例的方法
    public static SingletonLazy getInstance(){
        //判断instance实例是否存在,存在则返回
        if (instance == null) {
            //给if语句和new操作加锁,防止多new操作
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
    //提供一个private修饰的构造方法,保证唯一性
    private SingletonLazy(){};
}

总结:

  • 单例模式是在进程中有且仅有一份实例的模式。
  • 单例模式分为饿汉模式与懒汉模式。
  • 饿汉模式天然不存在线程不安全问题。
  • 懒汉模式存在线程不安全问题,因此需要进行加锁(synchronized)操作与防止指令重排序(volatile)操作。

🧑‍💻作者:一只爱打拳的程序猿,Java领域新星创作者,阿里云社区优质创造者。

🗃️文章收录于:Java多线程编程

🗂️JavaSE的学习:JavaSE

🗂️Java数据结构:数据结构与算法

 本篇博文到这里就结束了,感谢点赞、评论、收藏、关注~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/550065.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MAC移动硬盘无法识别解决方案(超详细图文教程)

目录 步骤1:先找到外接移动硬盘,在 Mac “终端”执行命令 步骤2:加载移动硬盘,可以在终端执行以下两个命令之一 步骤3:终止后台自动执行的“磁盘修复进程” 步骤4:手动执行磁盘修复命令 步骤5&#xf…

lwIP更新记01:全局互斥锁替代消息机制

从 lwIP-2.0.0 开始,在 opt.h 中多了一个宏开关 LWIP_TCPIP_CORE_LOCKING,默认使能。这个宏用于启用 内核锁定 功能,使用 全局互斥锁 实现。在之前,lwIP 使用 消息机制 解决 lwIP 内核线程安全问题。消息机制易于实现,…

基于Linux安装Docker

Docker官网:Docker Docs: How to build, share, and run applications | Docker Documentation 学习任何技术,一定要参考相应的官网学习,一定要参考官网学习!!! 目录 一、环境准备 1.1 配置源 1.1.1 下载…

Vue+uniapp桃源婚恋交友APP 安卓小程序 nodejs java python

小程序框架:uniapp 小程序开发软件:HBuilder X 开发模式:混合开发本文先提出了开发基于小程序的桃源婚恋交友APP系统的背景意义,然后通过功能性和非功能性分析阐述本系统的需求,然后进行系统设计。技术实现部分选择Jav…

c++ 11标准模板(STL) std::set(九)

定义于头文件 <set> template< class Key, class Compare std::less<Key>, class Allocator std::allocator<Key> > class set;(1)namespace pmr { template <class Key, class Compare std::less<Key>> using se…

vue参照企业微信日程写一个小组件

今天公司要求做日程 这体的话 和企业微信的日程功能挺想的 也没有找到特别好的工具 就直接自己手敲了一个 先看效果 因为样式使用 sass 写的 所以 项目中要引入 sass 感兴趣的 可以把代码拿去二开一下 <template><section class "skeletonPositioning"…

Java线程概述 (一)线程介绍

文章目录 &#x1f412;个人主页&#x1f3c5;JavaSE系列专栏&#x1f4d6;前言&#xff1a;&#x1fa85;什么是程序 、进程、线程&#xff1f;&#x1fa85;线程的生命周期&#x1fa85;多线程&#x1fa85;守护者线程&#x1fa85;线程并行与并发&#x1fa85;死锁&#x1f…

机器学习项目实战-能源利用率 Part-4(模型构建)

博主前期相关的博客可见下&#xff1a; 机器学习项目实战-能源利用率 Part-1&#xff08;数据清洗&#xff09; 机器学习项目实战-能源利用率 Part-2&#xff08;探索性数据分析&#xff09; 机器学习项目实战-能源利用率 Part-3&#xff08;特征工程与特征筛选&#xff09; 这…

AList挂载工具安装搭建使用教程,快速访问多个网盘的资源(保姆级图文)

目录 1. 下载AList2. 命令行启动快速启动小技巧 3. 用户登录4. 添加阿里云网盘帐号5. 添加百度云网盘资源总结 欢迎关注 『发现你走远了』 博客&#xff0c;持续更新中 欢迎关注 『发现你走远了』 博客&#xff0c;持续更新中 软件功能&#xff1a;将多个网盘的资源聚合在一起&…

本地部署 privateGPT

本地部署 privateGPT 1. 什么是 privateGPT2. Github 地址3. 创建虚拟环境4. 部署 privateGPT5. 配置 .env6. 下载模型7. 将文件放入 source_documents 目录中8. 摄取所有数据9. 向本地文档提问 1. 什么是 privateGPT 利用 GPT 的强大功能&#xff0c;私密地与您的文档交互&am…

Flutter 笔记 | Flutter 中的路由、包、资源、异常和调试

路由管理 Flutter中的路由通俗的讲就是页面跳转。在Flutter中通过Navigator组件管理路由导航。并提供了管理堆栈的方法。如&#xff1a;Navigator.push和Navigator.pop Flutter中给我们提供了两种配置路由跳转的方式&#xff1a;1、基本路由&#xff0c; 2、命名路由 普通路…

详解c++STL—函数对象

目录 1、函数对象 1.1、函数对象概念 1.2、函数对象的使用 2、谓词 2.1、谓词概念 2.2、一元谓词 2.3、二元谓词 3、内建函数对象 3.1、理解内建函数对象 3.2、算术仿函数 3.3、关系仿函数 3.4、逻辑仿函数 1、函数对象 1.1、函数对象概念 概念&#xff1a; 重载…

数据结构第三天 【二叉搜索树】

这道题真是写的我想吐了&#xff0c;主要是函数太多&#xff0c;排错太难了&#xff0c;搞了两个小时&#xff0c;基本就是在排错&#xff0c;排了一个小时&#xff0c;后面自己心态也有点崩溃了&#xff0c;其实不是一道很难的题&#xff0c;但是是一个非常麻烦的题目&#xf…

使用Serv-U搭建FTP服务器并公网访问

文章目录 1. 前言2. 本地FTP搭建2.1 Serv-U下载和安装2.2 Serv-U共享网页测试2.3 Cpolar下载和安装 3. 本地FTP发布3.1 Cpolar云端设置3.2 Cpolar本地设置 4. 公网访问测试5. 结语 转载自内网穿透工具的文章&#xff1a;使用Serv-U搭建FTP服务器并公网访问【内网穿透】_ 1. 前言…

linux专题:GDB详细调试方法与实现

系列文章目录 例如&#xff1a;第一章 Linux-GDB 调试实验的使用 文章目录 目录 系列文章目录 文章目录 一、实验目的 二、实验现象 三、实验准备 四、Linux GDB调试实验流程 五、Linux GDB 调试器 总结 一、实验目的 掌握使用 gcc 分步编译 c 代码为可执行程序步骤以及 gc…

【数学建模】步长的选择(优化建模)

人们每天都在行走&#xff0c;排除以运动健身为目的的走路方式&#xff0c;而仅仅考虑距离固定&#xff0c;以节省体力为最终目的的行走&#xff0c;那么选择多大的步长才最省力&#xff1f; 人在走路时所做的功等于抬高人体重心所需的势能与两腿运动所需的动能之和。在给定速度…

又到520了,来画一朵抽搐的玫瑰花吧

文章目录 静态的玫瑰 敲了这么多年代码&#xff0c;每年都得画一些心啊花啊什么的&#xff0c;所以现在常规的已经有些倦怠了&#xff0c;至少也得来个三维图形才看着比较合理&#xff0c;而且光是三维的也没啥意思&#xff0c;最好再加上能动起来。 静态的玫瑰 网上有很多生…

AIGC技术研究与应用 ---- 下一代人工智能:新范式!新生产力!(1-简介)

文章大纲 AI GC参考文献与学习路径模型进化券商研报陆奇演讲AI GC AI模型可大致分为决策式/分析式AI(Discriminant/Analytical AI)和生成式AI (Generative AI)两类。 决策式AI:学习数据中的条件概率分布,根据已有数据进行分析、判断、预测,主要应用模型有用于推荐系 统和…

Elasticsearch 集群部署插件管理及副本分片概念介绍

Elasticsearch 集群配置版本均为8以上 安装前准备 CPU 2C 内存4G或更多 操作系统: Ubuntu20.04,Ubuntu18.04,Rocky8.X,Centos 7.X 操作系统盘50G 主机名设置规则为nodeX.qingtong.org 生产环境建议准备单独的数据磁盘主机名 #各自服务器配置自己的主机名 hostnamectl set-ho…

chatgpt赋能Python-pythonf检验

Python的重要性与应用 Python是一种高级编程语言&#xff0c;因其简单易学和灵活性而备受欢迎。它已经成为数据分析、web开发、机器学习等许多领域的重要工具。在本篇文章中&#xff0c;我们将探讨Python在SEO中的作用。 Python对SEO的影响 SEO是搜索引擎优化的缩写&#xf…