多线程(四):线程安全
在开始讲解线程安全之前我们先来回顾一下我们学了那些东西了:
1. 线程和进程的认识
2. Thread 类的基本用法
3. 简单认识线程状态
4. 初见线程安全
上一章结束时看了一眼线程安全问题,本章将针对这个重点讲解。
一个代码在单线程中能够安全执行,但是在多线程中就容易出现错误;其本质原因就是线程在系统中的调度是无序的 / 抢占式执行的。
再看一眼上一章末尾的题,两个线程各执行 5w 次自增操作,最后的结果为什么是一个小于 10w 的随机数。
上节课也画了图:

线程不安全的原因
我们在这里讨论一下照成线程不安全的原因有哪些?
- 多线程的抢占式执行(罪魁祸首)
- 多个线程修改同一个变量 【如果是一个线程修改一个变量 => 安全】【多个线程读取一个变量 => 安全】【多个线程修改不同变量 => 安全】
- 修改操作不是原子的
- 内存可见性引起的线程不安全
- 指令重排序引起的线程不安全
那么我们就开始本章内容的讲解
对于 多线程的抢占式执行 和 多个线程修改同一个变量 这两点不是我们能够改变的,我们就直接跳过,直接看第三条
修改操作不是原子的
这里说到的原子性,数据库中 事物的原子性 是一个概念, 原子性意味着不可再分,说明每个操作都是最小单位。
例如上述例题: 每次自增操作都不算是最小操作,我们还可以对其进行划分,将一次 add 操作,分为三个小操作:load 、 add 、 save ;
任意某个操作对应单个 cpu 指令就是原子的, 对应多个 cpu 操作就是非原子的。
正是应该这个操作不是原子的,导致了俩个线程的指令排序存在更多的变数
既然我们发现了这个问题了,我们该如何解决呢?
保证操作的原子性
既然它不是原子的,那么我们就可以通过加锁操作让它变成原子性的。
就比如:
我们要上厕所,为了让别人也进来,所以需要锁门,我们就给门 加了个锁,那么上完厕所以后,就解锁,剩下的两个人就继续 抢占式 上厕所。
那么这个锁呢就可以保证 “原子性” 的效果
锁的核心操作就两个,加锁和解锁。
对于上述的一个锁,当谁抢到了,其他线程就需要等待,也就发生了 阻塞等待,直到拿到锁的线程释放为止。
那么如何对线程进行加锁呢?
加锁 和 解锁
Java提供了关键字:synchronized,Java直接用 synchronized 这个关键字实现加锁过程。
还是上一章中最后一段的线程自增 5w 次的例子:
代码如下
class Count {private int count = 0;public void add() {synchronized (this) {count++;}}public int get() {return count;}
}
public class demo11 {public static void main(String[] args) throws InterruptedException {Count count = new Count();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {count.add();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {count.add();}});t1.start();t2.start();t1.join();t2.join();System.out.println(count.get());}
}
唯一不同的点在于:

我们加了关键字。
这里给它加了个代码块,这个代码块有啥用呢?
一旦进入 被 synchronized 修饰的代码块时,就出发加锁机制, 一旦离开了这个代码块就会触发解锁机制。
而且我们在 synchronized 后面加了一个(this)这里的 this 就是锁对象。
谁调用 this 就是谁,就对谁进行加锁操作。
例如:

如果两个线程,针对同一个对象进行加锁,就会造成锁竞争(一个拿到锁,另一个线程阻塞等待)。如果两个对象针对不同的锁竞争就不会照成锁竞争。
现在重点来说一下锁括号里面的东西:
() 里的锁对象,可以是写作任意一个Object 对象,但是不能是 内置类型(内置类型就是基本数据类型)。
这括号主要就是为为了告诉大家,多个线程针对同一个对象加锁就会出现锁竞争,如果针对不同的对象加锁,就不会出现锁竞争了,再也没有别的作用
加锁以后,操作就变成原子的了,原来的操作就变成为了:

那么再次执行的时候就变成为了:

由于 t1 已经率先lock 了,t2 再次尝试 lock 就会出现阻塞等待的情况。
此时就可以保证 t2 的load 一定是在 t1 save 之后,此时计算的结果就一定是安全的。
加锁的本质其实就是变成串行化。
那么对比 join 方法,join也是实现串行化,join 方法是让两个线程都是实现串行化,而加锁只是让加锁的部分串行,其他部分还是并发执行的。
无论如何,加锁可能会造成阻塞,代码阻塞,对于程序的效率还是会有影响的。
内存可见性引起的线程不安全
我们先来写个 bug 在来说原因。
看代码:
import java.util.Scanner;public class demo12 {public static boolean flag = false;public static void main(String[] args) {Thread t1 = new Thread(() -> {while (!flag) {}});Thread t2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);flag = scanner.nextBoolean();});t1.start();t2.start();}
}
我们在来运行一遍:

可以看到输入了true 之后代码还在跑,同样可以在 jconsole 里看到线程还在执行,为什么这一段代码还继续执行呢。
这里就涉及到内存可见性了。
我们在执行这段代码的时候,进入到 while 循环, !flag 为真 在这个过程中又发生了两个 原子性的操作, 一个是 load :从内存读取数据到 cpu 寄存器;一个是 cmp (在cpu中可以叫别的名字):比较寄存器内的值是否为 false 。
这两个操作,load 消耗的时间远远高于 cmp 。
读内存虽然比读硬盘 快个几千倍 ; 读寄存器又要比 读内存快个几千倍
这样换算下来 每秒钟就要执行上亿次。
那么这样看下来,编译器发现 load 的开销很大,并且每次的结果都一样,那么编译器就做了一个非常大胆的操作,直接将 load 优化掉了(去掉了),只有第一次执行的 load 真正执行了,后续只循环 cmp 不执行 load 。
所谓的内存可见性就是在多线程的环境下,编译器对于代码优化,产生了误判,从而引起的 bug ,从而导致我们代码的 bug 。
那么我们就可以通过 让编译器对这个场景暂停优化 :
这里就需要使用另一个关键字: volatile
该关键字的含义就是:被它修饰的变量,此时编译器就会停止上述的优化。能够保证每次都是从内存上重新读取数据。
volatile关键字的作用主要有如下两个:
- 保证内存可见性:基于屏障指令实现,即当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
- 保证有序性:禁止指令重排序。编译时 JVM 编译器遵循内存屏障的约束,运行时靠屏障指令组织指令顺序。
volatile不能保证原子性,volatile 使用的场景是:一个线程读,一个线程写的情况,而 synchronized 则适用于多线程写。
volatile 的这个效果,称为 “保证内存可见性”。
而 synchronized 不确定是否也能保证内存可见性,网上资料 众说纷纭 。
volatile 还有一个效果,禁止指令重排序。
指令重排序
什么是指令重排序?
这也是编译器优化手段的一种,调整了代码的执行顺序,但是前后的逻辑不改变,效率更高。
如果是单线程的实现逻辑,结果并不会改变,但是在多线程中就会产生问题。
举例:
有个学生对象: Student s;
线程: t1 :s = new Student();
线程: t2 :if (s != null) s.learn();
大体可以分为三个步骤:
1. 申请内存空间
2. 调用构造方法(初始化内存的数据)
3. 把对象的引用赋值给s (内存地址的赋值)
如果是个单线程,此处可以发生指令重排序, 2 和 3 谁先谁后都可以。
t1执行1和3,即将执行2的时候,t2开始执行,t2拿到的就不是一个空的对象,是一个非空的,他就去调用cow的方法,但是实际上,t1还没有初始化,调用方法,会产生bug,所以我们可以在cow对象前加关键字volatile,保证执行顺序。
那么本章的 线程安全 就到这里,下一章继续多线程内容。
相关文章:
多线程(四):线程安全
在开始讲解线程安全之前我们先来回顾一下我们学了那些东西了: 1. 线程和进程的认识 2. Thread 类的基本用法 3. 简单认识线程状态 4. 初见线程安全 上一章结束时看了一眼线程安全问题,本章将针对这个重点讲解。 一个代码在单线程中能够安全执行&am…...
[ROC-RK3568-PC] [Firefly-Android] 10min带你了解Camera的使用
🍇 博主主页: 【Systemcall小酒屋】🍇 博主追寻:热衷于用简单的案例讲述复杂的技术,“假传万卷书,真传一案例”,这是林群院士说过的一句话,另外“成就是最好的老师”,技术…...
C++之模拟实现string
文章目录前言一、包含的相关头文件二、构造和析构1.构造函数2.拷贝构造1.传统写法2.现代写法3.赋值运算符重载1.传统写法2.现代写法4.析构函数三、iterator四、modify1.push_back(尾插一个字符)2.append(尾插一个字符串)3.运算符重载1.尾插字…...
SpringBoot实战(十三)集成 Admin
目录一、简介二、搭建 springboot-admin 管理服务1.Maven 依赖2.application.yml3.添加 EnableAdminServer4.启动服务,查看页面三、搭建 springboot-admin-client 客户端服务1.Maven 依赖2.application.yml3.启动服务,查看页面四、搭配 Eureka 使用1.搭建…...
mke2fs命令:建立ext2文件系统
以下内容源于网络资源的学习与整理,如有侵权请告知删除。 使用格式 mke2fs [options] [设备名称] [区块数] options与含义 -c:检查是否有损坏的区块。-F:不管指定的设备为何,强制执行mke2fs。-M:记录最后一次挂入的…...
免费分享一个springboot+vue的办公系统
springbootvue的OA系统项目介绍项目部署项目特点项目展示项目介绍 这是一个采用前后端分离开发的项目,前端采用 Vue 开发、后端采用 SpringBoot Mybatis 开发。 很适合java初学者练手和学习。 前端技术:Vue3.2 Vue-Router Pinia Ant Design Vue 3.X…...
STM32数据搬运工DMA
DMA的概念DMA,全称为:Direct Memory Access,即直接存储器访问。DMA 传输方式无需 CPU 直接控制传输,也没有中断处理方式那样保留现场和恢复现场的过程,通过硬件为 RAM 与 I/O 设备开辟一条直接传送数据的通路ÿ…...
4、操作系统——进程间通信(2)(system V-IPC介绍)
目录 一、system V-IPC常识 1、key和ID 2、文件描述符 3、函数(ftok) ftok产生IPC对象的健值key(类似文件路径) 4、例子 5、使用命令查看或删除当前系统中的IPC对象 一、system V-IPC常识 1、key和ID (1&#x…...
基于CentOS Stream 9平台搭建Nacos2.0.4集群以及OpenResty反向代理
目录展示Nacos2.0.4集群搭建1. 下载2. 解压3.修改配置3.1分别修改下启动类中JDK路径以及启动大小3.2 分别配置数据源3.3 创建nacos数据库3.4 修改cluster.conf配置3.4.1 复制并修改3.4.2 编辑文件,修改三台主机地址3.4.3 分别放入另外两个nacos的conf目录下:4. 启动…...
老杜MySQL入门基础 第二天
导入演示数据 1、连接MySQL 2、创建"bjpowernode"数据库 create database bjpowernode;3、选择数据库 use bjpowernode4、导入数据 source D:\bjpowernode.sql(文件的路径)1 去除重复记录(把查询结果去除重复记录)(原表数据不会改变) 使用关键字dist…...
Python深度学习实战:人脸关键点(15点)检测pytorch实现
引言 人脸关键点检测即对人类面部若干个点位置进行检测,可以通过这些点的变化来实现许多功能,该技术可以应用到很多领域,例如捕捉人脸的关键点,然后驱动动画人物做相同的面部表情;识别人脸的面部表情,让机…...
linux简单入门
目录Linux简介Linux目录结构Linux文件命令文件处理命令文件查看命令常用文件查看命令Linux的用户和组介绍Linux权限管理Linux简介 Linux,全称GNU/Linux,是一种免费使用和自由传播的类UNIX操作系统,其内核由林纳斯本纳第克特托瓦兹࿰…...
给准备面试网络工程师岗位的应届生一些建议
你听完这个故事,应该会有所收获。最近有一个23届毕业的大学生和我聊天,他现在网络工程专业大四,因为今年6、7月份的时候毕业,所以现在面临找工作的问题。不管是现在找一份实习工作,还是毕业后找一份正式工作࿰…...
主线程与子线程之间相互通信(HandlerThread)
平时,我们一般都是在子线程中向主线程发送消息(要在主线程更新UI),从而完成请求的处理。那么如果需要主线程来向子线程发送消息,希望子线程来完成什么任务。该怎么做?这就是这篇文章将要讨论的内容。 一、…...
13基于双层优化的电动汽车日前-实时两阶段市场竞标
MATLAB代码:基于双层优化的电动汽车日前-实时两阶段市场竞标 关键词:日前-实时市场竞标 电动汽车 双层优化 编程语言:MATLAB平台 参考文献:考虑电动汽车可调度潜力的充电站两阶段市场投标策略_詹祥澎 内容简介:…...
REDIS19_zipList压缩列表详解、快递列表 - QuickList、跳表 - SkipList
文章目录①. 压缩列表 - zipList②. 快递列表 - QuickList③. 跳表 - SkipList①. 压缩列表 - zipList ①. ZipList是一种特殊的"双端链表",由一系列特殊编码的连续内存块组成。可以在任意一端进行压入/弹出操作,并且该操作的时间复杂度为O(1) (oxff:11111111) type…...
JavaScript 基础 - 第3天
文章目录JavaScript 基础 - 第3天笔记数组数组的基本使用定义数组和数组单元数据单元值类型数组长度属性操作数组JavaScript 基础 - 第3天笔记 数组 数组的基本使用 定义数组和数组单元 <script>// 1. 语法,使用 [] 来定义一个空数组// 定义一个空数组let…...
23.3.26总结
康托展开 是一个全排列与自然数的映射关系,康托展开的实质是计算当前序列在所有从小到大的全排列中的顺序,跟其逆序数有关。 例如:对于 1,2,3,4,5 来说,它的康托展开值为 0*4!0*3!0*2!0*1&…...
【Java学习笔记】37.Java 网络编程
Java 网络编程 网络编程是指编写运行在多个设备(计算机)的程序,这些设备都通过网络连接起来。 java.net 包中 J2SE 的 API 包含有类和接口,它们提供低层次的通信细节。你可以直接使用这些类和接口,来专注于解决问题&…...
Azure OpenAI 官方指南03|DALL-E 的图像生成功能与安全过滤机制
2021年1月,OpenAI 推出 DALL-E。这是 GPT 模型在图像生成方面的人工智能应用。其名称来源于著名画家、艺术家萨尔瓦多 • 达利(Dal)和机器人总动员(Wall-E)。DALL-E 图像生成器,能够直接根据文本描述生成多…...
未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?
编辑:陈萍萍的公主一点人工一点智能 未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战,在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…...
深入剖析AI大模型:大模型时代的 Prompt 工程全解析
今天聊的内容,我认为是AI开发里面非常重要的内容。它在AI开发里无处不在,当你对 AI 助手说 "用李白的风格写一首关于人工智能的诗",或者让翻译模型 "将这段合同翻译成商务日语" 时,输入的这句话就是 Prompt。…...
Mybatis逆向工程,动态创建实体类、条件扩展类、Mapper接口、Mapper.xml映射文件
今天呢,博主的学习进度也是步入了Java Mybatis 框架,目前正在逐步杨帆旗航。 那么接下来就给大家出一期有关 Mybatis 逆向工程的教学,希望能对大家有所帮助,也特别欢迎大家指点不足之处,小生很乐意接受正确的建议&…...
uniapp微信小程序视频实时流+pc端预览方案
方案类型技术实现是否免费优点缺点适用场景延迟范围开发复杂度WebSocket图片帧定时拍照Base64传输✅ 完全免费无需服务器 纯前端实现高延迟高流量 帧率极低个人demo测试 超低频监控500ms-2s⭐⭐RTMP推流TRTC/即构SDK推流❌ 付费方案 (部分有免费额度&#x…...
【Zephyr 系列 10】实战项目:打造一个蓝牙传感器终端 + 网关系统(完整架构与全栈实现)
🧠关键词:Zephyr、BLE、终端、网关、广播、连接、传感器、数据采集、低功耗、系统集成 📌目标读者:希望基于 Zephyr 构建 BLE 系统架构、实现终端与网关协作、具备产品交付能力的开发者 📊篇幅字数:约 5200 字 ✨ 项目总览 在物联网实际项目中,**“终端 + 网关”**是…...
基于SpringBoot在线拍卖系统的设计和实现
摘 要 随着社会的发展,社会的各行各业都在利用信息化时代的优势。计算机的优势和普及使得各种信息系统的开发成为必需。 在线拍卖系统,主要的模块包括管理员;首页、个人中心、用户管理、商品类型管理、拍卖商品管理、历史竞拍管理、竞拍订单…...
push [特殊字符] present
push 🆚 present 前言present和dismiss特点代码演示 push和pop特点代码演示 前言 在 iOS 开发中,push 和 present 是两种不同的视图控制器切换方式,它们有着显著的区别。 present和dismiss 特点 在当前控制器上方新建视图层级需要手动调用…...
Modbus RTU与Modbus TCP详解指南
目录 1. Modbus协议基础 1.1 什么是Modbus? 1.2 Modbus协议历史 1.3 Modbus协议族 1.4 Modbus通信模型 🎭 主从架构 🔄 请求响应模式 2. Modbus RTU详解 2.1 RTU是什么? 2.2 RTU物理层 🔌 连接方式 ⚡ 通信参数 2.3 RTU数据帧格式 📦 帧结构详解 🔍…...
Visual Studio Code 扩展
Visual Studio Code 扩展 change-case 大小写转换EmmyLua for VSCode 调试插件Bookmarks 书签 change-case 大小写转换 https://marketplace.visualstudio.com/items?itemNamewmaurer.change-case 选中单词后,命令 changeCase.commands 可预览转换效果 EmmyLua…...
AxureRP-Pro-Beta-Setup_114413.exe (6.0.0.2887)
Name:3ddown Serial:FiCGEezgdGoYILo8U/2MFyCWj0jZoJc/sziRRj2/ENvtEq7w1RH97k5MWctqVHA 注册用户名:Axure 序列号:8t3Yk/zu4cX601/seX6wBZgYRVj/lkC2PICCdO4sFKCCLx8mcCnccoylVb40lP...
