Java 多线程(四)—— 线程安全 与 volatile 与 单例模式

什么是线程安全

在进行多线程编程的时候,当我们编写出来的多线程的代码运行结果不符合我们的预期的时候,这时候就是 bug,这种 bug 是由于多线程的问题而产生出来的 bug 我们称之为 线程安全问题

当我们编写出来的多线程代码运行之后的结果符合我们的预期结果的时候,说明代码没有问题,这时候就是 线程安全

线程安全问题的产生与解决方案

线程安全问题的产生主要有 五个原因

线程的调度是随机的

这个原因是由操作系统产生的,CPU 是多核心的,在进行线程的调度的时候并不是等到线程彻底执行完才轮到下一个线程执行,CPU 使用的是抢占式执行,也就是说,这个线程可能执行到一半,就立马被剥夺了 CPU 资源,开始执行下一个线程,然后执行完一半,又将上一个线程调度回来,这是由随机性的,程序员无法通过代码应用层得知。

这个问题是无法改变的,这也就是为什么会产生线程安全问题的最根本的原因

多个线程对同一个变量进行修改

在之前的文章中就已经设计过这种情况的讨论,如果修改的外部类的成员变量,是会发生线程安全问题的,如果修改的是局部变量,那就会触发 “变量捕获的语法”,这时候是不建议进行修改的。

解决方法也很简单,就是加锁,通过 synchronized 进行加锁。

public class Demo2 {private static int count = 0;public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {synchronized(locker) {count++;}}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {synchronized(locker) {count++;}}});t1.start();t2.start();t1.join();t2.join();System.out.println("count =" + count);}
}

线程的修改操作不是原子性的

这个问题其实和第二个问题是一样的,为什么修改同一个变量可能会发生线程安全问题,因为我们的修改指令并不是原子性的,也就说,这个操作并不是 CPU 执行一次指令就可以完成 count++ 的,count ++ 实质是由三条指令实现的,首先 load count 这个数值,然后进行 count +1 操作,最后将结果保存到内存里。

为了使修改操作是原子性的,所以我们使用加锁的方式来实现,也就是上面的代码。

内存可见性问题

这个问题是由于 JVM 优化而导致的,

public class Test {private static int flag = 1;public static void main(String[] args) {Thread t1 = new Thread(() -> {while(flag == 1) {}System.out.println("hello t1");});Thread t2 = new Thread(() -> {Scanner scan = new Scanner(System.in);System.out.println("请输入falg 的数值");flag = scan.nextInt();});t1.start();t2.start();}
}

在这里插入图片描述

即使我们修改了 flag 数值,但是程序依旧没有反应,说明在 t1 线程中读取到的 flag 依旧还是 1

一个线程涉及到了读操作,一个线程涉及到了修改操作,这可能会触发线程安全问题,也就是内存可见性问题,读操作没有读到修改过的数值。

原因:JVM / 编译器 其实是带有优化功能的,因为不同的程序员写出来的代码不同,运行效率也是不同,为了提高代码的运行效率,JVM / 编译器 在不改变我们代码的逻辑的情况下,会对我们写的代码进行优化。虽然说对我们代码逻辑不会做出改变,但是在多线程编程下可能会发生误判。

例如上面的代码,t1 线程进行读 flag 操作,也就是寄存器会从内存中读取 flag ,但是这是一个 while 循环,在一秒钟之内就会读取很多次,虽然 t2 线程会对 flag 进行修改,但是 t2 线程在启动之前 flag 这个数值就被 t1 线程读取了 几千万次,所以编译器 / JVM 会认为 flag 是一个不会被修改的数值,即把这个读内存操作优化为 读寄存器操作,也就是把 flag 这个数值拷贝一份到寄存器里,这样 CPU 就直接从寄存器读 flag 数值而不用到 内存中读取了。

等到了 t2 线程开始运行的时候,我们进行修改 flag 数值,内存中 flag 即使被修改了,但是 t1 线程还是不知道flag 被修改了,因为此时它是从寄存器读取 flag 数值。


拓展一下,如果我们在 t1 线程 加上 sleep 的话,这个内存可见性问题就消失了。

    private static int flag = 1;public static void main(String[] args) {Thread t1 = new Thread(() -> {while(flag == 1) {try {Thread.sleep(1);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("hello t1");});Thread t2 = new Thread(() -> {Scanner scan = new Scanner(System.in);System.out.println("请输入falg 的数值");flag = scan.nextInt();});t1.start();t2.start();}

在这里插入图片描述

即使是 sleep 1 ms 内存可见性问题也没有发生,这是为什么?

因为读内存操作可能就是几 ns 的事情,优化为 读寄存器操作可以再快个几 ns,但是代码存在 sleep 1 ms ,这个 1ms 的存在,编译器/ JVM 即使优化这个读操作也不能让代码的效率有一个质的飞跃,所以干脆就不提升了。所以内存可见性问题也就不存在了。

JVM / 编译器的优化是一个很复杂的事情,具体的细节大家可以参考深入理解Java虚拟机 这本书,在后续文章中也会提到 JVM 的部分内容。


如何解决这个内存可见性问题???
使用 volatile 关键字

在这里插入图片描述

这个关键字的英文翻译的易变的,说明这个变量我是会进行修改的,你不能进行读操作的优化。

注意这个关键字只能修饰变量,不能修饰方法!!!

修改后的代码:

import java.util.Scanner;public class Test {private static volatile int flag = 1;public static void main(String[] args) {Thread t1 = new Thread(() -> {while(flag == 1) {}System.out.println("hello t1");});Thread t2 = new Thread(() -> {Scanner scan = new Scanner(System.in);System.out.println("请输入falg 的数值");flag = scan.nextInt();});t1.start();t2.start();}
}

在这里插入图片描述


指令重排序问题

这个问题在下面的单例模式中的懒汉模式会提到~~

单例模式

单例模式是一种设计模式,也就是一个规范。

单例模式,顾名思义就是只允许一个对象的创建,也就是一个类只能创建实例化一个对象,不能进行多次实例化。这种设计模式的应用场景还是很多的,例如:我们在进行服务器开发的时候,我们需要一个对象来存放数据,这时候我们就会先写出类,然后再去创建对象,但是如果这个对象包含的数据很大,假如有100G,那么创建多次之后,也就是有几百G 的数据需要放在服务器上,并且这么多重复的数据也就只有一份是有用的,不仅仅是浪费了服务器的内存资源,还可能会导致服务器的崩溃,在这种情况下,我们通常使用单例模式来进行约束,只允许一个对象的创建。

饿汉模式

饿汉模式 是程序已启动,随着类的加载,对象也随之创建出来了,所以称之为 饿汉模式,说明创建的很快。

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

从上面的代码,我们就可以看到是要类一加载,对象instance 也就创建出来了private static Singleton instance = new Singleton();

为什么说我们不能进行多次创建呢?
因为这个类的构造方法被我们用private 修饰了,在外面是不能进行实例化的,这也是单例模式的点睛之笔。

我们来讨论一下,这个饿汉模式 的代码会不会出现线程安全问题?
答案是不会的,线程只是从getInstance() 进行读操作,获取 instance 这个对象,并没有涉及到修改操作,自然没有线程安全问题的存在。

懒汉模式

懒汉模式 顾名思义就是 懒,等我们真正需要这个对象的时候,才会进行实例化对象的操作。我们来看一下代码:

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

当我们真正需要用到这个对象的时候,才进行实例化,这就是懒汉模式。

但是在多线程编程下,是可能会出现线程安全问题,由于代码涉及到写操作,也就是 实例化对象的操作,假设有两个线程同时进行对象的实例化,就会发生线程安全问题,所以要加上锁 synchronized .

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

但是每次进行判断的时候都需要进行加锁,这就导致效率低下,所以我们在外面再加一层 if 判断,减少加锁的次数。

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

即使代码被我们修改成这样,还是会存在一个问题,指令重排序的问题

我们在实例化一个对象有三条指令需要做:第一申请内存空间,第二初始化对象,第三将内存空间的首地址赋值给引用。

在编译器/JVM 下可能会进行优化,将上面的三条指令优化为先执行1,再执行3 ,最后执行 2.

这可能会导致一个线程还没初始化对象,另一个线程就直接拿到这个对象进行使用了,但是这些使用操作,在后面的初始化完之后又被覆盖掉了。这就是第五个引起线程安全问题的原因 —— 指令重排序。

在这里插入图片描述

如何解决这个问题???
使用 volatile 关键字

没错 volatile 关键字不仅仅能解决内存可见性问题,还能解决指令重排序问题。

private static volatile SingletonLazy instance;

懒汉模式最终代码

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

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

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

相关文章

学习文档(5)

Redis应用 目录 Redis应用 Redis 除了做缓存&#xff0c;还能做什么&#xff1f; Redis 可以做消息队列么&#xff1f; Redis 可以做搜索引擎么&#xff1f; 如何基于 Redis 实现延时任务&#xff1f; Redis 除了做缓存&#xff0c;还能做什么&#xff1f; 分布式锁&…

三周精通FastAPI:5 查询参数和字符串校验

FastAPI手册&#xff1a;https://fastapi.tiangolo.com/zh/tutorial/query-params-str-validations/ 查询参数和字符串校验 FastAPI 允许你为参数声明额外的信息和校验。让我们以下面的应用程序为例&#xff1a; from fastapi import FastAPIapp FastAPI()app.get("/it…

基于springboot+thymeleaf+springsecurity搭建一套web小案例

一、前言 本案例中的源代码已上传到资源库&#xff0c;可自行下载&#xff0c;传送阵 https://download.csdn.net/download/qq_36260963/89906196 Spring Boot是为了简化Spring应用的创建、运行、调试、部署等而出现的&#xff0c;使用它可以做到专注于Spring应用的开发&#x…

git clone 鉴权失败

git clone 鉴权失败问题 1. 问题描述2. 解决方法 1. 问题描述 使用git clone自己的代码报如下错误&#xff1a; 正克隆到 xxx... Username for https://github.com: Password for https://xxxgithub.com: remote: Support for password authentication was removed on Augu…

RAG流程的实现与改进

一、 RAG流程图 数据入库&#xff1a;读取本地数据并切成小块&#xff0c;并把这些小块经过编码embedding后&#xff0c;存储在一个向量数据库中&#xff08;下图1——6步&#xff09;&#xff1b;相关性检索&#xff1a;用户提出问题&#xff0c;问题经过编码&#xff0c;再在…

Vue项目中实现拖拽上传附件:原生JS与Element UI组件方法对比

在现代化的Web应用中&#xff0c;文件上传是一个基本功能。随着技术的发展&#xff0c;拖拽上传已经成为提升用户体验的一个重要特性。在Vue项目中&#xff0c;我们可以通过原生JavaScript或使用Element UI组件来实现这一功能。下面我们将分别介绍这两种方法&#xff0c;并对比…

吴恩达深度学习笔记(6)

正交化 为了提高算法准确率&#xff0c;我们想到的方法 收集更多的训练数据增强样本多样性使用梯度下降将算法使算法训练时间更长换一种优化算法更复杂或者更简单的神经网络利用dropout 或者L2正则化改变网络框架更换激活函数改变隐藏单元个数 为了使有监督机制的学习系统良…

vue使用jquery的ajax,页面跳转

一、引入jquery依赖 打开终端更新npm npm install -g npm 更新完后引入输入npm install jquery 加载完后 在最外层的package.json文件中加入以下代码 配置好后导入jquery 设置变量用于接收服务器传输的数据 定义ajax申请数据 服务器的Controller层传输数据 &#xff08;…

【VUE小型网站开发】初始环境搭建

1. 初始化VUE项目 1.1 创建vue项目 1.2 删除多余的界面 根据自己情况删除红框内的文件 清理app页面代码 1.3 引入vue-router 1.3.1 下载vue-router npm install vue-router1.3.2 配置vue-router 在 main.js 或 main.ts 中引入 vue-router import ./assets/main.css im…

Android 图片相识度比较(pHash)

概述 在 Android 中&#xff0c;要比对两张 Bitmap 图片的相似度&#xff0c;常见的方法有基于像素差异、直方图比较、或者使用一些更高级的算法如 SSIM&#xff08;结构相似性&#xff09;和感知哈希&#xff08;pHash&#xff09;。 1. 基于像素的差异比较 可以逐像素比较…

基于MATLAB车牌识别系统设计

MATLAB车牌识别系统设计 实践目的 车牌是一辆汽车独一无二的信息&#xff0c;因此&#xff0c;对车辆牌照的识别技术可以作为 辨识一辆车最为有效的方法。随着ITS(智能交通系统)的高速发展&#xff0c;对车牌识别技术的研究也随之发展。从根本上讲&#xff0c;牌照识别应用了…

中缀表达式转后缀表达式(逆波兰表达式)及如何计算后缀表达式

目录 中缀、后缀表达式简介 中缀转后缀的规则 模拟中缀转后缀 中缀转后缀代码 后缀表达式求值 后缀表达式求值代码 Leetcode相关题目 中缀、后缀表达式简介 首先说说什么是中缀表达式&#xff0c;中缀表达式中&#xff0c;操作符是以中缀形式处于操作数的中间。例如&…

Linux安装Anaconda和Pytorch

又到了一年一度换环境、换服务器不断折腾的时节了&#xff0c;一通折腾后&#xff0c;重新启动遂做记录。 1. Linux安装Anaconda 1.1 离线安装模式 进入官网https://www.anaconda.com/download/success&#xff0c;如图所示&#xff1a; 选择版本进行下载即可。 1.2 在线w…

[Linux网络编程]03-TCP协议

一.TCP协议数据通信的过程 TCP数据报如下&#xff0c;数据报中的标志位双端通信的关键。 三次握手: 1.客户端向服务端发送SYN标志位&#xff0c;请求建立连接&#xff0c;同时发送空包 2.服务端向客户端回发ACK标志位(即确认标志位&#xff0c;任何一端发送数据后都需要另一端…

【VUE】【IOS】【APP】IOS Music APP播放器开发

前言 周末闲来无事&#xff0c;学习了下移动端的一些知识。了解到移动端的一些实现方式&#xff0c;先从最简单的开始。本人没有IOS swift 和Android的开发经验。抱着学习态度从简单的入手&#xff0c;经过了解&#xff0c;本人之前自己用vue的写着玩了几个小项目。看到可以用…

《使用Gin框架构建分布式应用》阅读笔记:p101-p107

《用Gin框架构建分布式应用》学习第7天&#xff0c;p101-p107总结&#xff0c;总计7页。 一、技术总结 1.StatusBadRequest vs StatusInternalServerError 写代码的时候有一个问题&#xff0c;什么时候使用 StatusBadRequest(400错误)&#xff0c;什么时候使用 StatusIntern…

1.2电子商务安全内涵

目录 1 电子商务安全的层次 2 计算机网络安全 3电子商务安全的特点 只有在你生命美丽的时候&#xff0c;世界才是美丽的。 —— 顾城 《顾城哲思录》 1 电子商务安全的层次 安全:主体没有危险的客观状态 电子商务安全是一个广泛的概念&#xff0c;它涉及到电子商务的各个方…

现今 CSS3 最强二维布局系统 Grid 网格布局

深入学习 CSS3 目前最强大的布局系统 Grid 网格布局 Grid 网格布局的基本认识 Grid 网格布局: Grid 布局是一个基于网格的二位布局系统&#xff0c;是目前 CSS 最强的布局系统&#xff0c;它可以同时对列和行进行处理&#xff08;它将网页划分成一个个网格&#xff0c;可以任…

PHP函数$_FILES详解

PHP函数$_FILES详解 在PHP中上传一个文件建一个表单要比ASP中灵活得多。具体的看代码。 <form enctype"multipart/form-data" action"upload.php" method"post"> <input type"hidden" name"MAX_FILE_SIZE" value…

嵌入式入门学习——8基于Protues仿真Arduino+SSD1306液晶显示数字时钟

0 系列文章入口 嵌入式入门学习——0快速入门&#xff0c;Let‘s Do It&#xff01; SSD1306 1 Protues查找SSD1306器件并放置在画布&#xff0c;画好电气连接&#xff08;这里VCC和GND画反了&#xff0c;后面仿真出错我才看见&#xff0c;要是现实硬件估计就烧毁了&#xf…