【Java 并发编程】单例模式

前言


        单例模式是一种十分常用但却相对而言比较简单的单例模式。虽然它简单但是包含了关于线程安全、内存模型、类加载机制等一些比较核心的知识点。本章会介绍单例模式的设计思想,会去讲解了几种常见的单例实现方式,如饿汉式、懒汉式、双重检锁、静态内部类、枚举等。


前期回顾:【Java 线程通信】模拟ATM取钱(wait 和 notify机制)


目录

前言

单例模式简介

 单例模式设计

饿汉式实现方式

代码分析

代码优劣

代码测试

懒汉式实现方式

代码优化

线程安全

效率低下 

双重检锁

内存可见性

完美代码

代码优劣

静态内部类的实现方式

枚举的实现方式

关于反射破坏

 

单例模式简介


        单例模式,顾名思义就是一个运行时域,一个类只有一个实例对象

        那么为什么需要单例模式呢?单例模式的使用场景是什么?

        像我们之前写的类的实例对象的创建与销毁对资源来说消耗不大,用不用单例模型其实无所谓。但是有些类的消耗比较大,如果频繁的创建与销毁而且这些类的对象完全可以复用的话,这势必会造成不必要的性能浪费

举个栗子~

        我们要写一个访问数据库类,但是创建数据库链接对象是一个十分耗资源的操作,并且数据库链接是完全可以复用的。那么可以把这个类设置为单例的,这样只需要创建一次对象并且重复使用这个对象就好了,而不用每次去访问数据库都要创建链接对象。

 

 单例模式设计


        单例模式有多种写法,比如饿汉式、懒汉式等等。但是不管是哪一种写法其实都要考虑一下三点:

是否线程安全
是否懒加载(也叫延迟加载)
能否反射破坏

 

饿汉式实现方式

class Singleton {private static Singleton instance = new Singleton();private Singleton() {}public static Singleton getInstance() {return instance;}
}

        因为这种方法在 类加载时 就立即创建了实例,就如饿汉看见吃的就 “迫不及待”的去吃的感觉。这里也是如此,类一加载就迫不及待的创建了对象,所以称之为饿汉。

 

代码分析

(1) 由于单例就是一个类只有一个实例对象的,所以我们并不希望别人能通过 new 直接创建对象,所以我们使用 private 来修饰构造方法

(2) 这个对象由于 static 静态修饰的,所以在类加载的时候就已经创建好了,通过 getInstance 调用只是获取这个对象实例而已,并且这种创建方式是天生线程安全的。

 

代码优劣

优点:

JVM 在加载这个类的时候就会对它进行初始化, 这里包含对静态变量的初始化,天生线程安全
没有加锁,运行效率更高


缺点:

类加载时就初始化,若是重启服务的话,会拖慢运行速度
类加载时就初始化,如果创建了不使用,会导致内存浪费

 

代码测试

class Test{public static void main(String[] args) {Singleton s1 = Singleton.getInstance();Singleton s2 = Singleton.getInstance();System.out.println(s1 == s2);}
}

运行结果

true

        所以饿汉的方式创建对象只会创建一个单例,考虑到空间浪费,使用的时候还需权衡优劣。

懒汉式实现方式

以下是只是标准模板(单线程版本),还有很多因素没有考虑 ~

class Singleton {private static Singleton instance = null;private Singleton() {}public static Singleton getInstance() {if (instance == null) {instance = new Singleton();}return instance;}
}

        以上代码我们可以发现这个对象并不会随着类加载而创建,而是在第一次访问单例类的实例时才去创建(第一次调用 getInstance 方法),我们将这种延迟创建的行为称之为 “懒汉”。

 

代码优化

线程安全

(1) 首先我们发现以上代码是线程不安全的,在执行以下这条语句时

if (instance == null)

可能会有多个线程已经越过这个语句去创建对象了,所以它是不安全的。

我们需要改进一下:

class Singleton {private static Singleton instance = null;private Singleton() {}public static synchronized Singleton getInstance() {if (instance == null) {instance = new Singleton();}return instance;}
}

 

效率低下 

(2) 先给这个方法加上 synchronized ,这样就能保证同一时刻只有一个线程访问这个方法了,但是这样又会引入新的问题:其实我们只想要对像构建的时候同步线程,像以上这种代码是每次在获取对象的时候都要进行同步操作,这样对性能影响是是十分大的。所以这种方法并不推荐。

        通过以上可以知道要想提升效率,直接在对象构建的时候加同步锁就可以了,而使用对象是不需要同步的,那么我们就可以改成这样。如下图:

class Singleton {private static Singleton instance = null;private Singleton() {}public static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {instance = new Singleton();}}return instance;}
}

 

双重检锁

(3) 关于上述代码,getInstance 是不需要参与锁竞争的所有线程都可直接进入,那么现在就开始第二步判断,如果实例对象没有创建,那么所有线程都会去争抢锁,抢到锁的那个线程会开始创建实例对象。实例对象创建了之后,以后所有的 getInstance 操作都是进行到第二步直接跳过,然后返回当前实例对象。这就解决了上述代码的低效问题。

        但是以上代码仍然是有问题的:

 if (instance == null) {synchronized (Singleton.class) {instance = new Singleton();}}

        假设 线程A、线程B 同时进入 if 语句,那么线程A拿到锁后创建了一个实例对象后将锁释放了;此时 线程B 拿到锁也可以创建实例对象。此时就可以创建多个实例对象了,所以这也是线程不安全的。有没有一种办法保证线程安全呢?其实我们只要在内部在加上一条 if 判断检查当前对象是否被创建即可。这种方法也被叫做双重检锁

class Singleton {private static Singleton instance = null;private Singleton() {}public static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;}
}

        此时 线程B 获得锁以后会进行一个判空,此时 线程A 已经实例过一次了,线程B 自然就不能创建对象了。

 

内存可见性

(4) 关于上述代码虽然看上去已经很完美了,但是还是有一点瑕疵。这里就要谈到 happens-before 内存可见性原则。简单来讲就是我们简单的一条 Java 语句,内部其实是有多种指令运行完成的。

        比如像以下这行代码,由于不是原子操作,虽然只是一条语句,但实际有三个指令在完成操作。

 instance = new Singleton();
(1)为对象分配内存
(2)初始化对象
(3)返回对象指向的内存地址

        以上的一条语句在非并发也就是单线程中是没有问题的,但是在并发执行时,虚拟机为了效率可能会对指令进行重排比如说 线程A 的执行顺序是:1->3->2。那么这个线程是先为对象分配好内存,再返回这个对象指向的内存地址,但是由于这个对象还没来得及初始化。此时如果有一个线程 B 进行 if (instance == null) 判空操作就会返回 false 跳过创建对象这个步骤,直接返回这个未初始化的对象。这也是造就了线程安全问题。那么怎么解决呢?我们只需加上 volatile 修饰即可。

 

完美代码
class Singleton {private static volatile Singleton instance = null;private Singleton() {}public static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;}
}

 

代码优劣

优点

需要这个实例的时候,先判断它是否为空,如果为空,再创建单例对象
用到的时候再去创建,避免了创建了对象不去的用而造成浪费

缺点

由于懒汉模式经过优化过后已经没有什么缺点了,唯一的缺点就是编写略显复杂。

关于其他的创建方式这里简述一下: 

静态内部类的实现方式

        JVM在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性、方法被调用时才会被加载,并初始化其静态属性

class StaticInnerSingleton {private StaticInnerSingleton() {}public static StaticInnerSingleton getInstance() {return SingletonHolder.instance;}private static class SingletonHolder {private static StaticInnerSingleton instance = new StaticInnerSingleton();}
}

        比较推荐这种方式,没有加锁,线程安全。用到时再加载,并发行能高。

枚举的实现方式

        枚举单例是最好的单例,有效防止反射

enum EnumSingleton {// 此枚举类的一个实例, 可以直接通过EnumSingleton.INSTANCE来使用INSTANCE
}

关于反射破坏

        以上的方式除了枚举,其他都能被放射破坏。但是反射是一种人为操作,只有故意去这样操作才会造成反射破坏。

class Singleton {private static volatile Singleton instance = null;private Singleton() {}public static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;}
}class Test1{public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {Singleton s1  = Singleton.getInstance();// 使用反射创建Singleton实例Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(null);declaredConstructor.setAccessible(true);// 通过反射获取的实例Singleton s2 = declaredConstructor.newInstance();System.out.println(s1 == s2);}
}

运行结果:

false

 关于如何利用反射破坏单例,请参考以上代码 ~

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【大模型】AI视频课程制作工具开发

1. 需求信息 1.1 需求背景 讲师们在制作视频的过程中&#xff0c;发现录制课程比较麻烦&#xff0c;要保证环境安静&#xff0c;保证录制过程不出错&#xff0c;很容易反复重复录制&#xff0c;为了解决重复录制的工作量&#xff0c;想通过 ai 课程制作工具&#xff0c;来解决…

数据轻松上云——Mbox边缘计算网关

随着工业4.0时代的到来&#xff0c;工厂数字化转型已成为提升生产效率、优化资源配置、增强企业竞争力的关键。我们凭借其先进的边缘计算网关与云平台技术&#xff0c;为工厂提供了高效、稳定的数据采集与上云解决方案。本文将为您介绍Mbox边缘计算网关如何配合明达云平台&…

【CS常见问题】你用的是VS2019,最高支持.NET5.0,但是项目将.NET6.0设为目标无法运行,怎么办?

.NET版本问题 报错示例报错分析最简单的方法步骤 报错示例 严重性 代码 说明 项目 文件 行 禁止显示状态 错误 NETSDK1045 当前 .NET SDK 不支持将 .NET 6.0 设置为目标。请将 .NET 5.0 或更低版本设置为目标&#xff0c;或使用支持 .NET 6.0 的 .NET SDK 版本。 ABFview C:\x…

◇【论文_20150225】 DQN_2015(nature) 〔Google DeepMind〕

整理代码 1&#xff1a;DQN CartPole_v1.ipynb https://www.nature.com/articles/nature14236 Human-level control through deep reinforcement learning 文章目录 摘要主体&#xff1a;要做什么 如何做的 要点keypoints实验 与 评估2 个指标和 各游戏的最好方法比较t-S…

【Linux网络编程】Socket编程--UDP(第一弹):实现客户端和服务器互相发送消息

&#x1f308;个人主页&#xff1a; 南桥几晴秋 &#x1f308;C专栏&#xff1a; 南桥谈C &#x1f308;C语言专栏&#xff1a; C语言学习系列 &#x1f308;Linux学习专栏&#xff1a; 南桥谈Linux &#x1f308;数据结构学习专栏&#xff1a; 数据结构杂谈 &#x1f308;数据…

.Net自动更新程序GeneralUpdate,适用于wpf,winfrom,控制台应用

GeneralUpdate是基于.net framwork4.5.2开发的一款&#xff08;c/s应用&#xff09;自动升级程序。 第一个版本叫Autoupdate 有人会奇怪为什么会改名称&#xff0c;稍微解释一下是因为在nuget上有重名的项目再者就是新版本更新功能不仅限于wpf程序的更新。 将更新的核心部分抽…

《Knowledge Perceived Multi-modal Pretraining in E-commerce》中文校对版

文章中文化系列目录 文章目录 文章中文化系列目录摘要CCS概念&#xff1a;关键词&#xff1a;1 引言2 相关工作2.1 多模态预训练2.2 知识图谱增强的预训练模型 3 方法3.1 模态编码层3.1.1 图像初始特征3.1.2 文本初始特征3.1.3 知识的表面形式特征 3.2 模态交互层3.2.1 图像模态…

day02 -- docker

1.docker的介绍 Docker 是一个开源的应用容器引擎&#xff0c;基于 Go语言 并遵从 Apache2.0 协议开源。Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中&#xff0c;然后发布到任何流行的 Linux 机器上&#xff0c;也可以实现虚拟化。容器是完全使…

填充与步幅

一个3x3的卷积核对6X6的图像进行卷积&#xff0c;得到一个4x4的图像 此时&#xff0c;生成图像长或宽 ln-f1 使用过滤器对图像进行卷积出现的问题有&#xff1a; 每次卷积后图像都会变小&#xff0c;当网络过深的时候&#xff0c;就会遗失许多像素 过滤器对边缘像素访问仅一…

基于C#开发游戏辅助工具的Windows底层相关方法详解

开发游戏辅助工具通常需要深入了解Windows操作系统的底层机制&#xff0c;以及如何与游戏进程进行有效交互。本文将基于C#语言&#xff0c;从Windows底层方法的角度来详细讲解开发游戏辅助工具的相关技术和概念。 一、游戏辅助工具的基本概述 游戏辅助工具&#xff0c;通常被称…

网络学习笔记

一、网络的结构与功能 网络的鲁棒性与抗毁性 如果在移走少量节点后网络中的绝大部分节点仍然是连通的&#xff0c;那么就该网络的连通性对节点故障具有鲁棒性 网络上的动力学 动力系统&#xff1a;自旋、振子或混沌的同步、可激发系统 传播过程&#xff1a;信息传播与拥堵…

git命令使用一览【自用】

git常见操作&#xff1a; git initgit remote add master【分支名字】 gitgits.xxxxx【仓库中获取的ssh链接或者http协议的链接】检查远程仓库是否链接成功。 git remote -v出现以下画面就可以git pull,git push了

可以与 FastAPI 不分伯仲的 Python 著名的 Web 框架

正如你所理解的&#xff0c;任何领域都不可能停止进步&#xff0c;不断使用相同的工具意味着不思进取。这一点在信息技术领域&#xff0c;尤其是网络开发行业非常明显。 关于网络框架&#xff0c;不论是 Django 和 Flask 等传统框架还是 Python 的新型高级框架&#xff0c;一直…

Semantic Kernel进阶:将ChatCompletion(聊天完成)服务添加到你的AI项目(三)

文章目录 Semantic Kernel进阶&#xff1a;将聊天完成服务添加到你的AI项目一、引言二、聊天完成服务的重要性三、基本介绍3.1 创建聊天完成服务3.2 依赖注入方式3.3 创建独立的服务实例 四、实战4.1 检索聊天完成服务4.2 使用聊天完成服务4.2.1 非流式4.2.2 流式 4.3 完整代码…

Mybatis多对一查询的配置及两种方法的使用示例对比以及Mybatis一对多查询两种方法使用示例及对比

一、Mybatis多对一查询的配置及两种方法的使用示例对比 为了试验Mybatis多对一的查询&#xff0c;我们先在数据库中建两个表&#xff0c;一个城市表&#xff0c;一个市区表&#xff0c;一个城市有多个区是一个一对多的关系&#xff1b;多个区对应一个城市是一个多对一的关系。建…

第五届光学与图像处理国际学术会议(ICOIP 2025)征稿中版面有限!

第五届光学与图像处理国际学术会议&#xff08;ICOIP 2025&#xff09; 2025 5th International Conference on Optics and Image Processing (ICOIP 2025&#xff09; 重要信息 时间地点&#xff1a;2025年4月25-27日丨中国西安 截稿日期&#xff1a;2024年12月16日23:59 …

vue3项目使用百度地图实现地图选择功能代码封装(开箱即用)

vue3项目使用百度地图实现地图选择功能代码封装方案(开箱即用) <template><div class="bmapgl">

【软件测试】JUnit

Junit 是一个用于 Java 编程语言的单元测试框架&#xff0c;Selenium是自动化测试框架&#xff0c;专门用于Web测试 本篇博客介绍 Junit5 文章目录 Junit 使用语法注解参数执行顺序断言测试套件 Junit 使用 本篇博客使用 Idea集成开发环境 首先&#xff0c;创建新项目&#…

一图解千言,了解常见的流程图类型及其作用

在企业管理、软件研发过程中&#xff0c;经常会需要进行各种业务流程梳理&#xff0c;而流程图就是梳理业务时必要的手段&#xff0c;同时也是梳理的产出。但在不同的情况下适用的流程图又不尽相同。 本文我们就一起来总结一下8 种最常见的流程图类型 数据流程图 数据流程图&…

【CTF-SHOW】Web入门 Web14 【editor泄露-详】【var/www/html目录-详】

editor泄露问题通常出现在涉及文件编辑器或脚本编辑器的题目中&#xff0c;尤其是在Web安全或Pwn&#xff08;系统漏洞挖掘&#xff09;类别中。editor泄露的本质是由于系统未能妥善处理临时文件、编辑历史或进程信息&#xff0c;导致攻击者可以通过某种途径获取正在编辑的敏感…