当前位置: 首页 > news >正文

Java 多线程系列Ⅱ(线程安全)

线程安全

  • 一、线程不安全
    • 线程不安全的原因:
  • 二、线程不安全案例与解决方案
    • 1、修改共享资源
      • synchronized 使用
      • synchronized 特性
    • 2、内存可见性
      • Java内存模型(JMM)
      • 内存可见性问题
    • 3、指令重排列
    • 4、synchronized 和 volatile
    • 5、拓展知识:修饰符顺序规范

一、线程不安全

如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。否则就称之为线程不安全。

线程不安全的原因:

  1. 抢占式执行,可以说“线程的无序调度”是罪魁祸首,万恶之源!!!(是操作系统内核来实现的,程序员无法控制)
  2. 多个线程修改同一个变量。
  3. 修改操作,不是原子(不可分割的最小单位)的。某个操作对应单个cpu指令就是原子的,如果单个操作对应多个CPU指令,大概率不是原子的。正是因为不是原子的,导致多个线程的指令排列存在更多的变数。
  4. 内存可见性,引起的线程不安全。
  5. 指令重排列,引起的线程不安全。

二、线程不安全案例与解决方案

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 特性

  1. 进入 synchronized 修饰的代码块, 相当于 加锁。 退出 synchronized 修饰的代码块, 相当于 解锁。
  2. synchronized修饰的代码块具有原子性效果。即加锁是让多个线程的某个部分进行串行。
  3. synchronized()其中()里的对象,可以是任意一个Object对象,这个对象也被称为锁对象。synchronized用的锁是存在Java对象头里的,可以粗略理解成:每个对象在内存中存储的时候, 都存有一块内存表示当前的 “锁定” 状态,如果当前是 “解锁” 状态, 那么就可以使用, 使用时需要设为 “加锁” 状态,如果当前是 “加锁” 状态, 那么其他线程无法使用, 只能阻塞等待
  4. synchronized是互斥锁,所谓互斥,即同一时间多个线程不能对同一对象加锁。而是同一时刻只能有一个线程获取锁,其他线程阻塞等待。因此多个线程尝试对同一个锁对象加锁,此时就会产生锁竞争,针对不同对象加锁,就不会有锁竞争。
  5. 阻塞等待:针对每一把锁, 操作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝试进行加锁, 就加不上了, 就会阻塞等待, 一直等到之前的线程解锁之后, 由操作系统唤醒一个新的线程, 再来获取到这个锁。
  6. 获取锁原则:上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 “唤醒”。 这也就是操作系统线程调度的一部分工作.。假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则。
  7. 拓展:synchronized 既是悲观锁,也是乐观锁。既是轻量级锁,也是重量级锁。轻量级锁部分基于自旋锁实现,重量级锁部分基于挂起等待锁实现。是互斥锁不是读写锁,是非公平锁。(后续介绍)

2、内存可见性

Java内存模型(JMM)

介绍内存可见性之前,我们先简单了解一下java内存模型:

  • 工作内存-work memory :CPU寄存器 + 缓存
  • 主内存-main memory :内存
  1. 线程之间的共享变量存在 主内存 (Main Memory).
  2. 每一个线程都有自己的 “工作内存” (Working Memory) .
  3. 当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据.
  4. 当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存.

为什么引入工作内存?

这里引入工作内存主要是因为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步:

  1. 申请内存空间
  2. 调用构造方法(初始化内存数据)
  3. 把对象的引用赋值给s(内存地址的赋值)

如果是单线程下,上述操作很容易保证,如果是多线程下,指令2,3重排先执行3后执行2,在刚执行完指令3后,t2线程执行s.learn();就会出现bug。

解决方案

  1. 当前场景下可使用volatile修饰,因为volatile具有防止指令重拍的作用,可以解决上述可能出现的问题。
  2. 可以对new操作加锁-synchronized

4、synchronized 和 volatile

  1. synchronized 保证原子性,volatile 不保证原子性。
  2. 一般情况下 volatile 适用于一个线程读一个线程写的情况。
  3. 一般情况下 synchronized 适用于多个线程写的情况。

5、拓展知识:修饰符顺序规范

在Java中,修饰符的顺序可以任意排列,但是为了方便阅读和代码的一致性,一般会按照以下的顺序进行排列:

  1. 可见性修饰符(public, protected, private)
  2. 非可见性修饰符(static, final, abstract)
  3. 类型修饰符(class, interface, enum)
  4. 其他修饰符(synchronized, transient, volatile,native, strictfp)

相关文章:

Java 多线程系列Ⅱ(线程安全)

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

const用法详解

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

【LeetCode75】第四十二题 删除二叉搜索数中的节点

目录 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 代码&#xff1a; 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 题目给我们一棵二叉搜索树&#xff0c;给我们一个目标值&#xff0c;让我们删除节点值等于目标值的节点&#xff0c;并且删除之后需要保持…...

c++:QT day2 信号和槽

1.多态&#xff1a; 静态多态&#xff1a;函数的重载 动态多态&#xff1a;程序运行 多态的实现:父类的指针或引用&#xff0c;指向或初始化子类的对象&#xff0c;调用子类对父类重写的函数&#xff0c;进而展开子类的功能 2.虚函数&#xff1a;用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。可能会用到的资料有如下所示&#xff0…...

AI人员打闹监测识别算法

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

如何使用CRM系统进行精细化管理客户?

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

20230829工作心得:如何把大List 切割为多个小List?

1 怎么看这个方法谁在调用它&#xff1f; 解决&#xff1a;按ctrl&#xff0c;然后点进去看。 idea里看方法的具体细节&#xff0c;和谁在调用这个方法&#xff0c;都可以通过按ctrl&#xff0c;然后单击查看。 2 请求的时候&#xff0c;如果时间yyyy-MM-dd HH:mm:ss在url里会…...

基于YOLOV8模型的阶梯和工人目标检测系统(PyTorch+Pyside6+YOLOv8模型)

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

Nginx特性应用及载装

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

vue3+ts组件通信

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

基于卷积优化算法优化的BP神经网络(预测应用) - 附代码

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

《论文阅读18》JoKDNet

一、论文 研究领域&#xff1a;用于大尺度室外TLS点云配准的联合关键点检测和特征表达网络论文&#xff1a;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); //听说你很喜欢数学&#xff0c;不知道你是否爱它胜过爱flag if(!isset($_GET[c])){show_source(__FILE__); }else{//例子 c20-1$content $_GET[c];if (strlen($content) > 80) {…...

GraphQL渗透测试案例及防御办法

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

Hive SQL 优化大全(参数配置、语法优化)

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

go锁-waitgroup

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

访问0xdddddddd内存地址引发软件崩溃的问题排查

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

Java从入门到精通-流程控制(一)

流程控制 1.复合语句 复合语句&#xff0c;也称为代码块&#xff0c;是一组Java语句&#xff0c;用大括号 {} 括起来&#xff0c;它们可以被视为单个语句。复合语句通常用于以下情况&#xff1a; - 在控制结构&#xff08;如条件语句和循环&#xff09;中包含多个语句。 - …...

MybatisPlus(2)

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

css实现圆环展示百分比,根据值动态展示所占比例

代码如下 <view class""><view class"circle-chart"><view v-if"!!num" class"pie-item" :style"{background: conic-gradient(var(--one-color) 0%,#E9E6F1 ${num}%),}"></view><view v-else …...

1688商品列表API与其他数据源的对接思路

将1688商品列表API与其他数据源对接时&#xff0c;需结合业务场景设计数据流转链路&#xff0c;重点关注数据格式兼容性、接口调用频率控制及数据一致性维护。以下是具体对接思路及关键技术点&#xff1a; 一、核心对接场景与目标 商品数据同步 场景&#xff1a;将1688商品信息…...

镜像里切换为普通用户

如果你登录远程虚拟机默认就是 root 用户&#xff0c;但你不希望用 root 权限运行 ns-3&#xff08;这是对的&#xff0c;ns3 工具会拒绝 root&#xff09;&#xff0c;你可以按以下方法创建一个 非 root 用户账号 并切换到它运行 ns-3。 一次性解决方案&#xff1a;创建非 roo…...

基于 TAPD 进行项目管理

起因 自己写了个小工具&#xff0c;仓库用的Github。之前在用markdown进行需求管理&#xff0c;现在随着功能的增加&#xff0c;感觉有点难以管理了&#xff0c;所以用TAPD这个工具进行需求、Bug管理。 操作流程 注册 TAPD&#xff0c;需要提供一个企业名新建一个项目&#…...

嵌入式学习笔记DAY33(网络编程——TCP)

一、网络架构 C/S &#xff08;client/server 客户端/服务器&#xff09;&#xff1a;由客户端和服务器端两个部分组成。客户端通常是用户使用的应用程序&#xff0c;负责提供用户界面和交互逻辑 &#xff0c;接收用户输入&#xff0c;向服务器发送请求&#xff0c;并展示服务…...

Golang——9、反射和文件操作

反射和文件操作 1、反射1.1、reflect.TypeOf()获取任意值的类型对象1.2、reflect.ValueOf()1.3、结构体反射 2、文件操作2.1、os.Open()打开文件2.2、方式一&#xff1a;使用Read()读取文件2.3、方式二&#xff1a;bufio读取文件2.4、方式三&#xff1a;os.ReadFile读取2.5、写…...

Chromium 136 编译指南 Windows篇:depot_tools 配置与源码获取(二)

引言 工欲善其事&#xff0c;必先利其器。在完成了 Visual Studio 2022 和 Windows SDK 的安装后&#xff0c;我们即将接触到 Chromium 开发生态中最核心的工具——depot_tools。这个由 Google 精心打造的工具集&#xff0c;就像是连接开发者与 Chromium 庞大代码库的智能桥梁…...

人工智能--安全大模型训练计划:基于Fine-tuning + LLM Agent

安全大模型训练计划&#xff1a;基于Fine-tuning LLM Agent 1. 构建高质量安全数据集 目标&#xff1a;为安全大模型创建高质量、去偏、符合伦理的训练数据集&#xff0c;涵盖安全相关任务&#xff08;如有害内容检测、隐私保护、道德推理等&#xff09;。 1.1 数据收集 描…...

Vue ③-生命周期 || 脚手架

生命周期 思考&#xff1a;什么时候可以发送初始化渲染请求&#xff1f;&#xff08;越早越好&#xff09; 什么时候可以开始操作dom&#xff1f;&#xff08;至少dom得渲染出来&#xff09; Vue生命周期&#xff1a; 一个Vue实例从 创建 到 销毁 的整个过程。 生命周期四个…...

C#中用于控制自定义特性(Attribute)

我们来详细解释一下 [AttributeUsage(AttributeTargets.Class, AllowMultiple false, Inherited false)] 这个 C# 属性。 在 C# 中&#xff0c;Attribute&#xff08;特性&#xff09;是一种用于向程序元素&#xff08;如类、方法、属性等&#xff09;添加元数据的机制。Attr…...