Java 多线程系列Ⅱ(线程安全)
线程安全
- 一、线程不安全
- 线程不安全的原因:
- 二、线程不安全案例与解决方案
- 1、修改共享资源
- synchronized 使用
- synchronized 特性
- 2、内存可见性
- Java内存模型(JMM)
- 内存可见性问题
- 3、指令重排列
- 4、synchronized 和 volatile
- 5、拓展知识:修饰符顺序规范
一、线程不安全
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。否则就称之为线程不安全。
线程不安全的原因:
- 抢占式执行,可以说“线程的无序调度”是罪魁祸首,万恶之源!!!(是操作系统内核来实现的,程序员无法控制)
- 多个线程修改同一个变量。
- 修改操作,不是原子(不可分割的最小单位)的。某个操作对应单个cpu指令就是原子的,如果单个操作对应多个CPU指令,大概率不是原子的。正是因为不是原子的,导致多个线程的指令排列存在更多的变数。
- 内存可见性,引起的线程不安全。
- 指令重排列,引起的线程不安全。
二、线程不安全案例与解决方案
1、修改共享资源
即针对于多个线程修改同一个变量的情况,由于修改操作可能不是原子的(单条cpu指令),在多线程的随机调度下,就会导致多个线程的指令排列存在更多变数。
例如如下代码:
class Counter {private int count = 0;public void add() {count++;}public int getCount() {return count;}
}public class ThreadExample_unsafe {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.add();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.add();}});t1.start();t2.start();//等两个线程结束后查看结果t1.join();t2.join();System.out.println(counter.getCount());}
}
结果分析:
对于如上代码,两个线程 t1、t2 各自对 count 自增 50000 次,理论情况下结果应为100000,但是实际运行结果小于100000,尽管多次运行依旧如此。以上现象正是因为,在 t1、t2 两个线程修改 count 时,由于每个 ++ 操作都不是原子的,可以分割为(1.读取 2.修改 3.写入),在系统随机调度的加持下就会导致 t1、t2 线程++操作实际指令排列顺序有多种可能,最终导致结果异常。如下图绘制了两种可能出现的情况:
解决方案-加锁
对于以上场景,在保证并发执行的情况下,由于线程的随机调度是系统内核来实现的,程序员不可控,而多个线程修改同一变量又是业务需求,所以要保证该场景下的线程安全我们可以考虑将修改操作变成原子的。而“加锁”可以保证原子性效果。synchronized
是 Java 中用于实现锁的关键字,下面我们详细介绍:
synchronized 使用
Java中使用synchronized
针对“对象头”
加锁,synchronized 势必要搭配一个具体的对象来使用。
(1)synchronized对普通方法加锁
// 给实例方法加锁
public void add() {synchronized (this) {count++;}
}//如果直接给方法使用synchronized修饰,此时就相当于以this为锁对象
synchronized public void add() {count++;
}
(2)synchronized对静态方法加锁
//给静态方法加锁
public static void test2() {// Counter.class相当于类对象synchronized (Counter.class) {}
}
//如果直接给方法使用synchronized修饰,此时就相当于以Counter.class为锁对象
synchronized public static void test() {}
(3)synchronized对任意代码块加锁
// 自定义锁对象
Object locker = new Object();synchronized (locker) {// 代码逻辑// . . .
}
拓展:被 synchronized
修饰的方法又叫同步方法;被 synchronized
修饰的代码块又叫同步代码块。
synchronized 特性
- 进入 synchronized 修饰的代码块, 相当于 加锁。 退出 synchronized 修饰的代码块, 相当于 解锁。
- synchronized修饰的代码块具有原子性效果。即加锁是让多个线程的某个部分进行串行。
- synchronized()其中()里的对象,可以是任意一个Object对象,这个对象也被称为锁对象。synchronized用的锁是存在Java对象头里的,可以粗略理解成:每个对象在内存中存储的时候, 都存有一块内存表示当前的 “锁定” 状态,如果当前是 “解锁” 状态, 那么就可以使用, 使用时需要设为 “加锁” 状态,如果当前是 “加锁” 状态, 那么其他线程无法使用, 只能
阻塞等待
- synchronized是互斥锁,所谓互斥,即同一时间多个线程不能对同一对象加锁。而是同一时刻只能有一个线程获取锁,其他线程阻塞等待。因此多个线程尝试对同一个锁对象加锁,此时就会产生锁竞争,针对不同对象加锁,就不会有锁竞争。
- 阻塞等待:针对每一把锁, 操作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝试进行加锁, 就加不上了, 就会阻塞等待, 一直等到之前的线程解锁之后, 由操作系统唤醒一个新的线程, 再来获取到这个锁。
- 获取锁原则:上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 “唤醒”。 这也就是操作系统线程调度的一部分工作.。假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则。
- 拓展:synchronized 既是悲观锁,也是乐观锁。既是轻量级锁,也是重量级锁。轻量级锁部分基于自旋锁实现,重量级锁部分基于挂起等待锁实现。是互斥锁不是读写锁,是非公平锁。(后续介绍)
2、内存可见性
Java内存模型(JMM)
介绍内存可见性之前,我们先简单了解一下java内存模型:
- 工作内存-work memory :CPU寄存器 + 缓存
- 主内存-main memory :内存
- 线程之间的共享变量存在 主内存 (Main Memory).
- 每一个线程都有自己的 “工作内存” (Working Memory) .
- 当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据.
- 当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存.
为什么引入工作内存?
这里引入工作内存主要是因为CPU 访问自身寄存器的速度以及高速缓存的速度, 远远超过访问内存的速度。在某些情况下,这也是提高效率的一种重要手段。比如某个代码中要连续 10000 次读取某个变量的值, 如果 10000 次都从内存读, 速度是很慢的。但是如果只是第一次从内存读, 读到的结果缓存到 CPU 的某个寄存器中, 那么后 9999 次读数据就不必直接访问内存了。效率就大大提高了。
内存可见性问题
内存可见性是指当一个线程修改了某个变量的值,其它线程总是能知道这个变量变化。也就是说,如果线程 A 修改了共享变量 V 的值,那么线程 B 在使用 V 的值时,能立即读到 V 的最新值。
什么是内存可见性引起的多线程安全问题?
一般来说由内存可见性引发的多线程问题,是由于编译器的优化。例如:
public class ThreadExample_unsafe2 {public static int flag = 0;public static void main(String[] args) {Thread t1 = new Thread(()->{while (flag == 0) {//空转}System.out.println("循环结束,t1结束!");});Thread t2 = new Thread(()->{Scanner scanner = new Scanner(System.in);System.out.print("请输入一个整数:");flag = scanner.nextInt();});t1.start();t2.start();}
}
结果分析
如上代码,t1线程中flag == 0涉及到两个CPU指令,假设这两个指令分别是load-从内存读取数据到工作内存(CPU寄存器),cmp-比较寄存器中的值是否为0。对于这两个操作,load的时间开销远远高于cmp。此时编译器在处理的时候发现,load的开销很大,每次load的结果都一样,此时编译器就做了一个非常大胆的决定,即只有第一次load执行从内存读取到工作内存,后续循环的load直接从工作内存读取。所以尽管输入了不为0的整数,因为工作内存数据不变,程序依然继续运行。
关于编译器优化:
针对以上线程安全问题,是编译器优化的结果,关于编译器优化,这是一个很普遍的事,编译器优化就是能够智能调整你代码的执行逻辑,保证程序结果不变的情况下,通过加减语句,通过语句变换等一系列操作,让整个程序的执行效率大大提升。但是对于编译器优化在单线程情况下一般是不会出现任何问题的,但是多线程下不能保证。
解决方案
使用volatile修饰:被关键字volatile修饰的变量,此时编译器就会禁止例如上述优化,能够保证每次都是从内存重新读取数据到工作内存,保证了内存可见性。
3、指令重排列
指令重排,也是程序优化的一种手段,和编译器的优化有直接的关系,也和线程不安全直接相关。如果是单线程的情况下,这样的调整没问题,但是在多线程的情况下就会发生线程安全问题。
例如下面伪代码:
其中线程t1中s = new Student();
大体可以分为3步:
- 申请内存空间
- 调用构造方法(初始化内存数据)
- 把对象的引用赋值给s(内存地址的赋值)
如果是单线程下,上述操作很容易保证,如果是多线程下,指令2,3重排先执行3后执行2,在刚执行完指令3后,t2线程执行s.learn();就会出现bug。
解决方案
- 当前场景下可使用volatile修饰,因为volatile具有防止指令重拍的作用,可以解决上述可能出现的问题。
- 可以对new操作加锁-synchronized
4、synchronized 和 volatile
synchronized
保证原子性,volatile
不保证原子性。- 一般情况下 volatile 适用于一个线程读一个线程写的情况。
- 一般情况下 synchronized 适用于多个线程写的情况。
5、拓展知识:修饰符顺序规范
在Java中,修饰符的顺序可以任意排列,但是为了方便阅读和代码的一致性,一般会按照以下的顺序进行排列:
- 可见性修饰符(public, protected, private)
- 非可见性修饰符(static, final, abstract)
- 类型修饰符(class, interface, enum)
- 其他修饰符(synchronized, transient, volatile,native, strictfp)
相关文章:

Java 多线程系列Ⅱ(线程安全)
线程安全 一、线程不安全线程不安全的原因: 二、线程不安全案例与解决方案1、修改共享资源synchronized 使用synchronized 特性 2、内存可见性Java内存模型(JMM)内存可见性问题 3、指令重排列4、synchronized 和 volatile5、拓展知识…...

const用法详解
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言一、const用法详解二、使用步骤 1.引入库2.读入数据总结 前言 提示:这里可以添加本文要记录的大概内容: 例如:随着人工智能…...

【LeetCode75】第四十二题 删除二叉搜索数中的节点
目录 题目: 示例: 分析: 代码: 题目: 示例: 分析: 题目给我们一棵二叉搜索树,给我们一个目标值,让我们删除节点值等于目标值的节点,并且删除之后需要保持…...

c++:QT day2 信号和槽
1.多态: 静态多态:函数的重载 动态多态:程序运行 多态的实现:父类的指针或引用,指向或初始化子类的对象,调用子类对父类重写的函数,进而展开子类的功能 2.虚函数:用virtua关键字修饰的函数是虚函…...

16 Linux之JavaEE定制篇-搭建JavaEE环境
16 Linux之JavaEE定制篇-搭建JavaEE环境 文章目录 16 Linux之JavaEE定制篇-搭建JavaEE环境16.1 概述16.2 安装JDK16.3 安装tomcat16.4 安装idea2020*16.5 安装mysql5.7 学习视频来自于B站【小白入门 通俗易懂】2021韩顺平 一周学会Linux。可能会用到的资料有如下所示࿰…...

AI人员打闹监测识别算法
AI人员打闹监测识别算法通过yolopython网络模型框架算法, AI人员打闹监测识别算法能够准确判断出是否有人员进行打闹行为,算法会立即发出预警信号。Yolo算法,其全称是You Only Look Once: Unified, Real-Time Object Detection,其…...

如何使用CRM系统进行精细化管理客户?
客户是企业的生命线,对客户进行精细化管理,是提高企业收益的关键。那么,如何进行客户管理?CRM系统可以实现精细化管理客户,提升客户的价值。下面我们就来详细说一说。 1、获取客户信息 Zoho CRM系统可以通过web表单、…...

20230829工作心得:如何把大List 切割为多个小List?
1 怎么看这个方法谁在调用它? 解决:按ctrl,然后点进去看。 idea里看方法的具体细节,和谁在调用这个方法,都可以通过按ctrl,然后单击查看。 2 请求的时候,如果时间yyyy-MM-dd HH:mm:ss在url里会…...

基于YOLOV8模型的阶梯和工人目标检测系统(PyTorch+Pyside6+YOLOv8模型)
摘要:基于YOLOV8模型的阶梯和工人目标检测系统可用于日常生活中检测与定位工人、梯子目标,利用深度学习算法可实现图片、视频、摄像头等方式的目标检测,另外本系统还支持图片、视频等格式的结果可视化与结果导出。本系统采用YOLOv8目标检测算…...

Nginx特性应用及载装
Nginx是一款轻量级的Web 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器。其特点是占有内存少,并发能力强,事实上nginx的并发能力在同类型的网页服务器中表现较好,中国大陆使用nginx的网站有:网易、腾讯、阿里等。 …...

vue3+ts组件通信
1、父组件向组件传参 父组件代码 子组件代码 2、子组件向父组件传参 组件间代码 父组件代码 3、如果eslint报错,需在.eslintrc.js中添加一行代码 4、通过父组件通过 ref 获取子组件的属性或者方法 父组件代码 子组件代码 5、孙子组件provide和inject 父组件…...

基于卷积优化算法优化的BP神经网络(预测应用) - 附代码
基于卷积优化算法优化的BP神经网络(预测应用) - 附代码 文章目录 基于卷积优化算法优化的BP神经网络(预测应用) - 附代码1.数据介绍2.卷积优化优化BP神经网络2.1 BP神经网络参数设置2.2 卷积优化算法应用 4.测试结果:5…...

《论文阅读18》JoKDNet
一、论文 研究领域:用于大尺度室外TLS点云配准的联合关键点检测和特征表达网络论文:JoKDNet: A joint keypoint detection and description network for large-scale outdoor TLS point clouds registration International Journal of Applied Earth Ob…...

buuctf [CISCN 2019 初赛]Love Math
这题主要利用了php的一些特性 变量函数数字转字符串 源码 <?php error_reporting(0); //听说你很喜欢数学,不知道你是否爱它胜过爱flag if(!isset($_GET[c])){show_source(__FILE__); }else{//例子 c20-1$content $_GET[c];if (strlen($content) > 80) {…...

GraphQL渗透测试案例及防御办法
什么是GraphQL GraphQL 是一种 API 查询语言,旨在促进客户端和服务器之间的高效通信。它使用户能够准确指定他们在响应中所需的数据,从而有助于避免有时使用 REST API 看到的大型响应对象和多个调用。 GraphQL 服务定义了一个合约,客户端可…...

Hive SQL 优化大全(参数配置、语法优化)
文章目录 参数配置优化yarn-site.xml 配置文件优化mapred-site.xml 配置文件优化 分组聚合优化 —— Map-Side优化参数解析优化案例 服务器环境说明 机器名称内网IP内存CPU承载服务master192.168.10.1084NodeManager、DataNode、NameNode、JobHistoryServer、Hive、HiveServer…...

go锁-waitgroup
如果被等待的协程没了,直接返回 否则,waiter加一,陷入sema add counter 被等待协程没做完,或者没人在等待,返回 被等待协程都做完,且有人在等待,唤醒所有sema中的协程 WaitGroup实现了一组协程…...

访问0xdddddddd内存地址引发软件崩溃的问题排查
目录 1、问题描述 2、访问空指针或者野指针 3、常见的异常值 4、0xdddddddd内存访问违例问题分析与排查 5、关于0xcdcdcdcd和0xfeeefeee异常值的排查案例 6、最后 VC常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)ht…...

Java从入门到精通-流程控制(一)
流程控制 1.复合语句 复合语句,也称为代码块,是一组Java语句,用大括号 {} 括起来,它们可以被视为单个语句。复合语句通常用于以下情况: - 在控制结构(如条件语句和循环)中包含多个语句。 - …...

MybatisPlus(2)
前言🍭 ❤️❤️❤️SSM专栏更新中,各位大佬觉得写得不错,支持一下,感谢了!❤️❤️❤️ Spring Spring MVC MyBatis_冷兮雪的博客-CSDN博客 上篇我们简单介绍了MybatisPlus的方便之处,这篇来深入了解Myb…...

iOS UITableView上拉加载解决偶然跳动的Bug
最近做项目,测试测出来一个Bug,列表添加了上拉刷新和下拉加载,当我弹窗消失时,调用刷新列表后,在某个手机型号上,偶发列表刷新跳动的bug。(一般在列表上拉加载刷新到最后一页后,再弹窗消失,reload列表,会出现此bug) Bug复现如下:RPReplay_Final1693296737 解决方案…...

MySQL 外键使用详解
1、MySQL 外键约束语法 MySQL 支持外键,允许在表之间进行相关数据的交叉引用,并有助于保持相关数据的一致性。 一个外键关系涉及到一个父表,该父表保存初始列值,和一个子表,子表的列值引用父表的列值。外键约束定义在…...

MongoDB实验——在MongoDB集合中查找文档
在MongoDB集合中查找文档 一、实验目的二、实验原理三、实验步骤1.启动MongoDB数据库、启动MongoDB Shell客户端2.数据准备-->person.json3.指定返回的键4 .包含或不包含 i n 或 in 或 in或nin、$elemMatch(匹配数组)5.OR 查询 $or6.Null、$exists7.…...

事务的总结
数据库事务 数据库事务是一个被视为单一的工作单元的操作序列。这些操作应该要么完整地执行,要么完全不执行。事务管理是一个重要组成部分,RDBMS 面向企业应用程序,以确保数据完整性和一致性。事务的概念可以描述为具有以下四个关键属性描述…...

[ROS]yolov5-7.0部署ROS
YOLOv5是一种目标检测算法,它是YOLO(You Only Look Once)系列算法的最新版本。与其它目标检测算法相比,YOLOv5在速度和准确性方面取得了显著的提升。在ROS(Robot Operating System)中使用Python部署YOLOv5可…...

Java抽象方法、抽象类和接口——第七讲
前言 上一讲,我们深入了解面向对象,介绍了面向对象有三个特征——封装、继承、多态,以及介绍方法的重载和重写,这些都是开发中很常用的特征,基本都尊重面向对象思想。再上一讲我们了解到了继承的时候,子类要重新写父类的方法,才能遵循子类的规则,那么忘记重写怎么办呢?…...

kafka集群之kraft模式
一、概要 Kafka作为一种高吞吐量的分布式发布订阅消息系统,在消息应用中广泛使用,尤其在需要实时数据处理和应用程序活动跟踪的场景,kafka已成为首选服务;在Kafka2.8之前,Kafka强依赖zookeeper来来负责集群元数据的管理…...

虹科案例 | 缆索挖掘机维护—小传感器,大作用!
一、 应用背景 缆索挖掘机 缆索挖掘机的特点是具有坚固的部件,如上部结构、回转环和底盘。底盘是用于移动挖掘机的下部机械部件,根据尺寸和型号的不同,由轮子或履带引导,并承载可转动的上部车厢。回转环连接上部和下部机器部件&am…...

Windows安装FFmpeg说明
下载地址 官网 Download FFmpeg Csdn ffmpeg安装包,ffmpeg-2023-08-28-git-b5273c619d-full-build.7z资源-CSDN文库 解压安装,添加环境变量 命令行输入ffmpeg 安装成功...

电子电路原理题目整理(1)
电子电路原理题目整理(1) 最近在学习《电子电路原理》,记录一下书后面试题目,答案为个人总结,欢迎讨论。 1.电压源和电流源的区别? 电压源在不同的负载电阻下可提供恒定的负载电压,而电流源对于…...