【Java多线程】线程安全及解决方案(详解)
目录
线程安全问题引入:
线程安全原因
如何解决线程安全问题?
(1)synchronized关键字
1)sychronized关键字的特性:
2)可重⼊
synchronized使⽤⽰例
(2)volatile关键字
1)内存可见性和指令重排序
2) volatile不保证原⼦性
3)volatile总结:
线程安全问题引入:
public class demo2 {public static int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{for (int i = 0; i < 10000; i++) {count++;}});Thread t2 = new Thread(()->{for (int i = 0; i < 10000; i++) {count++;}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}
}
执行结果:
count=14423执行结果:
count=10223执行结果:
count=12123
由此可见,我们正常情况下想得到的结果为20000,而现在的结果却不尽人意 很明显在此情况下代码出现了bug。
因为多个线程并发执行,引起的bug,这样的bug称为“线程安全问题”或者叫做“线程不安全”
线程安全原因
问题分析:
(1)线程在操作系统中是随机调度,抢占式执行的【根本原因】
(2)当前代码中多个线程修改一个变量
多个线程修改同⼀个变量
上⾯的线程不安全的代码中,涉及到多个线程针对 count 变量进⾏修改.
此时这个 count 是⼀个多个线程都能访问到的"共享数据
(3)修改操作,不是“原子”的
不保证原⼦性会给多线程带来什么问题
如果⼀个线程正在对⼀个变量操作,中途其他线程插⼊进来了,如果这个操作被打断了,结果就可能是错误的。
count++操作实际上分成三步:
1)load 从内存中读取数据到cpu的寄存器
2)add 把寄存器中的值+1
3)save 把寄存器的值写回内存中
而由于线程调度是随机调度,抢占式执行的,这就导致了两个线程的count++操作三步骤是会被打乱顺序的。
还有其他原因稍后介绍...
如何解决线程安全问题?
(1)synchronized关键字
1)sychronized关键字的特性:
synchronized会起到互斥效果,某个线程执⾏到某个对象的synchronized中时,其他线程如果也执⾏到同⼀个对象synchronized就会阻塞等待
• 进⼊synchronized修饰的代码块,相当于加锁
• 退出synchronized修饰的代码块,相当于解锁
public class demo2 {public static int count = 0;public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1 = new Thread(()->{synchronized (locker) {for (int i = 0; i < 10000; i++) {count++;}}});Thread t2 = new Thread(()->{synchronized (locker) {for (int i = 0; i < 10000; i++) {count++;}}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}
}
通过对count++的整体加锁,使得每一次的count++都是一个整体,解决了此处的线程安全问题。
加锁的过程:
可以粗略理解成,每个对象在内存中存储的时候,都存有⼀块内存表⽰当前的"锁定"状态(类似于厕所
的"有⼈/⽆⼈").
如果当前是"⽆⼈"状态,那么就可以使⽤,使⽤时需要设为"有⼈"状态.
如果当前是"有⼈"状态,那么其他⼈⽆法使⽤,只能排队
- 上⼀个线程解锁之后,下⼀个线程并不是⽴即就能获取到锁.⽽是要靠操作系统来"唤醒".这也就是操作系统线程调度的⼀部分⼯作.
- 假设有ABC三个线程,线程A先获取到锁,然后B尝试获取锁,然后C再尝试获取锁,此时B和C都在阻塞队列中排队等待.但是当A释放锁之后,虽然B⽐C先来的,但是B不⼀定就能获取到锁,⽽是和C重新竞争,并不遵守先来后到的规则.
2)可重⼊
synchronized同步块对同⼀条线程来说是可重⼊的,不会出现⾃⼰把⾃⼰锁死的问题
按照之前对于锁的设定,第⼆次加锁的时候,就会阻塞等待.直到第⼀次的锁被释放,才能获取到第⼆个锁.但是释放第⼀个锁也是由该线程来完成,结果这个线程已经躺平了,啥都不想⼲了,也就⽆法进⾏解锁操作.这时候就会死锁
for (int i = 0; i < 50000; i++) {
synchronized (locker) {
synchronized (locker) {count++;}}
}
public class demo2 {public static int count = 0;public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1 = new Thread(()->{synchronized (locker) {synchronized (locker) {for (int i = 0; i < 10000; i++) {count++;}}}});Thread t2 = new Thread(()->{synchronized (locker) {synchronized (locker) {for (int i = 0; i < 10000; i++) {count++;}}}});t1.start();t2.start();t1.join();t2.join();System.out.println("count="+count);}
}
count=20000
synchronized使⽤⽰例
1)修饰代码块:明确指定锁哪个对象.
锁任意对象
public class SynchronizedDemo {private Object locker = new Object();public void method() {synchronized (locker) {}}
}
锁当前对象
public class SynchronizedDemo {public void method() {synchronized (this) {}}
}
2)直接修饰普通⽅法:锁的SynchronizedDemo对象
public class SynchronizedDemo {public synchronized void methond() {}
}
3)修饰静态⽅法:锁的SynchronizedDemo类的对象
public class SynchronizedDemo {public synchronized static void method() {}
}
我们重点要理解,synchronized锁的是什么.两个线程竞争同⼀把锁,才会产⽣阻塞等待.
两个线程分别尝试获取两把不同的锁,不会产⽣竞争.
(2)volatile关键字
1)内存可见性和指令重排序
还有一直情况就是“内存可见性”和“指令重排序”引发的线程安全
下列代码原本用意是:当用户输入非0数字时,结束线程t1。
import java.util.Scanner;
public class demo3 {private 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(() -> {System.out.println("请输入 flag 的值: ");Scanner scanner = new Scanner(System.in);flag = scanner.nextInt();});t1.start();t2.start();}
}
在这个代码中
• 创建两个线程t1和t2
• t1中包含⼀个循环,这个循环以flag==0为循环条件.
• t2中从键盘读⼊⼀个整数,并把这个整数赋值给flag.可结果是:
t1读的是⾃⼰⼯作内存中的内容.
当t2对flag?变量进⾏修改,此时t1感知不到flag的变化.
//如果给flag加上volatile
private static volatile int flag = 0;
import java.util.Scanner;
public class demo3 {private static volatile int flag = 0;public static void main(String[] args) {Thread t1 = new Thread(() -> {while (flag == 0) {// 循环体里, 啥都不写会触发内存可见性问题}System.out.println("t1 线程结束!");});Thread t2 = new Thread(() -> {System.out.println("请输入 flag 的值: ");Scanner scanner = new Scanner(System.in);flag = scanner.nextInt();});t1.start();t2.start();}
}
2)volatile不保证原⼦性
public class demo3 {private static volatile int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {count++;}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {count++;}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}
}
count=68741
此时可以看到,最终count的值仍然⽆法保证是100000.
3)volatile总结:
1. 内存可见性
在多线程环境下,每个线程都有自己的本地内存(例如 CPU 的寄存器和缓存),当线程修改一个变量时,修改的内容可能只会被保存在线程的本地内存中,而不是立即刷新到主内存中。这可能导致其他线程读取该变量时,看到的是过时的数据。
volatile
关键字确保了:
当一个线程修改
volatile
变量时,修改后的值会立即刷新到主内存。其他线程读取这个
volatile
变量时,会直接从主内存中读取最新的值,而不是从自己的本地缓存中读取。2. 禁止指令重排
volatile
变量还具有防止指令重排的效果。指令重排是现代处理器为了提高性能,在程序执行时调整指令执行顺序的行为。它可能导致程序出现一些非预期的结果,特别是在多线程编程中。
volatile
关键字保证了变量的读写顺序不会被重排,因此它能确保代码在多个线程中的执行顺序符合预期。3. 不具备原子性
volatile
关键字虽然可以确保内存可见性和禁止指令重排,但并不具备原子性。也就是说,如果对一个volatile
变量进行复合操作(例如:i++
),它并不能保证操作的原子性。因为复合操作通常涉及多个步骤,如读取值、修改值、写回主内存,这些步骤不能保证在多线程环境下不会被打断。因此,volatile
只能用于简单的赋值操作。
synchronized关键字vs volatile关键字
特性 | synchronized | volatile |
---|---|---|
作用 | 提供互斥性和可见性,确保一个线程执行某个方法时,其他线程不能访问被保护的代码块。 | 仅保证变量的可见性,确保一个线程对该变量的修改能够立刻反映到其他线程。 |
性能开销 | 存在较高的性能开销,因为它涉及到线程的加锁和解锁操作。 | 相对较低的性能开销,只在访问volatile 变量时有较小的性能影响。 |
原子性 | 提供原子性,确保多线程在同一时刻只会有一个线程访问同步方法 或同步代码块 。 | 不保证原子性,仅仅保证可见性。例如 volatile 不能保证 ++ 操作的原子性。 |
使用场景 | 适用于需要保证原子性和互斥访问的场景,如多个线程同时修改共享变量。 | 适用于多线程间共享变量的简单读写场景,特别是标志位和开关。 |
总结
synchronized
:适用于需要保证线程安全和原子性操作的场景,通过加锁来保证一个线程在执行某个代码块时,其他线程无法同时访问这些代码,从而避免并发问题。volatile
:适用于保证多线程之间共享变量的可见性,尤其是用于标志位等简单的读写操作,但它不保证操作的原子性,因此不适用于需要进行复合操作的情况。在实际应用中,如果一个变量的读写操作不涉及复杂的计算和操作,且只需要保证它的可见性,可以考虑使用
volatile
。如果需要对共享资源进行复杂操作(如累加、修改多个共享变量等),则需要使用synchronized
来保证互斥性和原子性。
结语: 写博客不仅仅是为了分享学习经历,同时这也有利于我巩固知识点,总结该知识点,由于作者水平有限,对文章有任何问题的还请指出,接受大家的批评,让我改进。同时也希望读者们不吝啬你们的点赞+收藏+关注,你们的鼓励是我创作的最大动力!
相关文章:

【Java多线程】线程安全及解决方案(详解)
目录 线程安全问题引入: 线程安全原因 如何解决线程安全问题? (1)synchronized关键字 1)sychronized关键字的特性: 2)可重⼊ synchronized使⽤⽰例 (2)volatile关键字 1)内存可见性和…...
【前端基础】Javascript取整函数以及向零取整方式
向零取整方式 在JavaScript中,有多种方式可以对数字进行取整操作,即去掉小数部分,只保留整数部分。其中,向0取整(也称为截断小数部分)的方式有以下几种常用的方法: 使用 Math.trunc()ÿ…...

禅道与Jira与Ones对比:哪个更适合你的项目管理需求?
一、项目管理工具的重要性 在当今复杂的项目环境中,选择合适的项目管理工具对项目成功至关重要。随着项目规模的不断扩大、涉及领域的日益广泛以及团队成员的分散性,传统的项目管理方式已经难以满足需求。 项目管理工具可以帮助团队更好地规划和组织项…...
Linux I/O编程:I/O多路复用与异步 I/O对比
文章目录 0. 引言1. I/O 模型简介1.1 阻塞 I/O(Blocking I/O)1.2 非阻塞 I/O(Non-Blocking I/O)1.3 信号驱动式 I/O(Signal-Driven I/O)1.4 多路复用 I/O(I/O Multiplexing)1.5 异步…...
Spark Plan 之 SQLMetric
SQLMetric Spark Plan 包含以下基本 方法, /*** return All metrics containing metrics of this SparkPlan.*/def metrics: Map[String, SQLMetric] Map.empty/*** return [[SQLMetric]] for the name.*/def longMetric(name: String): SQLMetric metrics(name)…...

基于YOLOv5模型的火焰识别系统
大家好,YOLOv5模型能够快速准确地检测到火灾火焰,在火灾初期甚至是刚刚出现火苗时就发出警报。这为及时采取灭火措施争取了宝贵的时间,极大地降低了火灾造成的损失。系统可以对特定区域进行持续实时监测,无论白天还是夜晚…...

多模态AI:开启人工智能的新纪元
在人工智能的璀璨星河中,多模态AI技术正逐渐成为一颗耀眼的明星。随着科技的飞速发展,AI技术正以前所未有的速度迈向新的高峰,其中多模态AI的兴起尤为引人注目。本文将深入探讨多模态AI的定义、技术原理、应用场景以及未来发展趋势。 ps.图…...

麒麟信安支撑2024年电力监控系统网络安全加固培训护航电力网络安全!
在网络安全形势日益复杂的今天,电力行业的网络安全尤为重要。为提升电力监控系统网络安全运维人员的专业技能,由国调中心网安处精心策划,国家电网技术学院组织开展的“2024年电力监控系统网络安全加固培训”于近日圆满结束。麒麟信安作为重要…...

横表和纵表 中的横表
图1 图2...

7个常用的JavaScript数组操作进阶用法
文章目录 1、查找数组中的最大值方法一:使用 Math.max 和展开运算符方法二:使用 for 循环逐一比较 2、查找数组中的第二大值方法一:排序后取第二大值方法二:遍历找到第二大值 3、去除数组中的重复项4、合并两个有序数组并保持有序5、旋转数组…...

Spark的Standalone集群环境安装
一.简介 与MR对比: 概念MRYARNSpark Standalone主节点ResourceManagerMaster从节点NodeManagerWorker计算进程MapTask,ReduceTaskExecutor 架构:普通分布式主从架构 主:Master:管理节点:管理从节点、接…...
Android Glide动态apply centerCropTransform(),transition withCrossFade动画,Kotlin
Android Glide动态apply centerCropTransform(),transition withCrossFade动画,Kotlin import android.graphics.Bitmap import android.os.Bundle import android.widget.ImageView import androidx.appcompat.app.AppCompatActivity import com.bumptech.glide.Glide import …...

shukla方差和相对平均偏差
参考资料:实用统计学【李奉令】 Eberhart-Russell模型、Shukla模型、相对平均偏差稳定性分析比较 相对平均偏差在品种稳定性分析中的作用 1、Shukla方差 生物统计中,用于描述一个群体离散程度的统计量有离差、方差、极差等, 国内品种区域试…...

双指针(二)双指针到底是怎么个事
一.有效的三角形个数 有效的三角形个数 class Solution {public int triangleNumber(int[] nums) {Arrays.sort(nums);int i0,end nums.length-1;int count 0;for( i end;i>2;i--){int left 0;int right i-1;while(left<right){if(nums[left]nums[right]>nums…...

vscode通过remote-ssh连接远程开发机
文章目录 安装扩展注意事项:tips其他参数安装扩展 安装VS Code和SSH-Remote扩展:首先,需要确保你已经在本地计算机上安装了VS Code,并且在扩展市场中搜索并安装了"Remote - SSH"扩展。配置SSH:在本地计算机上,打开VS Code的命令面板(使用快捷键"Ctrl+Shi…...

uniapp实现H5和微信小程序获取当前位置(腾讯地图)
之前的一个老项目,使用 uniapp 的 uni.getLocation 发现H5端定位不准确,比如余杭区会定位到临平区,根据官方文档初步判断是项目的uniapp的版本太低。 我选择的方式不是区更新uniapp的版本,是直接使用高德地图的api获取定位。 1.首…...

SQL HAVING子句
SQL 是一种基于“面向集合”思想设计的语言。HAVING 子句是一个聚合函数,用于过滤分组结果。 1 实践 1.1 缺失的编号 图 连续编号记录表t_seq_record 需求:判断seq 列编号是否有缺失。 SELECT 存在缺失的编号 AS res FROM t_seq_record HAVING COUN…...
计算机视觉基础:OpenCV库详解
💓 博客主页:瑕疵的CSDN主页 📝 Gitee主页:瑕疵的gitee主页 ⏩ 文章专栏:《热点资讯》 计算机视觉基础:OpenCV库详解 计算机视觉基础:OpenCV库详解 计算机视觉基础:OpenCV库详解 引…...

UI自动化测试工具(超详细总结)
🍅 点击文末小卡片 ,免费获取软件测试全套资料,资料在手,涨薪更快 常用工具 1、QTP:商业化的功能测试工具,收费,可用于web自动化测试 2、Robot Framework:基于Python可扩展的关…...

AJAX 全面教程:从基础到高级
AJAX 全面教程:从基础到高级 目录 什么是 AJAXAJAX 的工作原理AJAX 的主要对象AJAX 的基本用法AJAX 与 JSONAJAX 的高级用法AJAX 的错误处理AJAX 的性能优化AJAX 的安全性AJAX 的应用场景总结与展望 什么是 AJAX AJAX(Asynchronous JavaScript and XML…...
【SpringBoot】100、SpringBoot中使用自定义注解+AOP实现参数自动解密
在实际项目中,用户注册、登录、修改密码等操作,都涉及到参数传输安全问题。所以我们需要在前端对账户、密码等敏感信息加密传输,在后端接收到数据后能自动解密。 1、引入依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId...

新能源汽车智慧充电桩管理方案:新能源充电桩散热问题及消防安全监管方案
随着新能源汽车的快速普及,充电桩作为核心配套设施,其安全性与可靠性备受关注。然而,在高温、高负荷运行环境下,充电桩的散热问题与消防安全隐患日益凸显,成为制约行业发展的关键瓶颈。 如何通过智慧化管理手段优化散…...
数据库分批入库
今天在工作中,遇到一个问题,就是分批查询的时候,由于批次过大导致出现了一些问题,一下是问题描述和解决方案: 示例: // 假设已有数据列表 dataList 和 PreparedStatement pstmt int batchSize 1000; // …...

分布式增量爬虫实现方案
之前我们在讨论的是分布式爬虫如何实现增量爬取。增量爬虫的目标是只爬取新产生或发生变化的页面,避免重复抓取,以节省资源和时间。 在分布式环境下,增量爬虫的实现需要考虑多个爬虫节点之间的协调和去重。 另一种思路:将增量判…...
2023赣州旅游投资集团
单选题 1.“不登高山,不知天之高也;不临深溪,不知地之厚也。”这句话说明_____。 A、人的意识具有创造性 B、人的认识是独立于实践之外的 C、实践在认识过程中具有决定作用 D、人的一切知识都是从直接经验中获得的 参考答案: C 本题解…...
管理学院权限管理系统开发总结
文章目录 🎓 管理学院权限管理系统开发总结 - 现代化Web应用实践之路📝 项目概述🏗️ 技术架构设计后端技术栈前端技术栈 💡 核心功能特性1. 用户管理模块2. 权限管理系统3. 统计报表功能4. 用户体验优化 🗄️ 数据库设…...

Razor编程中@Html的方法使用大全
文章目录 1. 基础HTML辅助方法1.1 Html.ActionLink()1.2 Html.RouteLink()1.3 Html.Display() / Html.DisplayFor()1.4 Html.Editor() / Html.EditorFor()1.5 Html.Label() / Html.LabelFor()1.6 Html.TextBox() / Html.TextBoxFor() 2. 表单相关辅助方法2.1 Html.BeginForm() …...

tauri项目,如何在rust端读取电脑环境变量
如果想在前端通过调用来获取环境变量的值,可以通过标准的依赖: std::env::var(name).ok() 想在前端通过调用来获取,可以写一个command函数: #[tauri::command] pub fn get_env_var(name: String) -> Result<String, Stri…...
Monorepo架构: Nx Cloud 扩展能力与缓存加速
借助 Nx Cloud 实现项目协同与加速构建 1 ) 缓存工作原理分析 在了解了本地缓存和远程缓存之后,我们来探究缓存是如何工作的。以计算文件的哈希串为例,若后续运行任务时文件哈希串未变,系统会直接使用对应的输出和制品文件。 2 …...

图解JavaScript原型:原型链及其分析 | JavaScript图解
忽略该图的细节(如内存地址值没有用二进制) 以下是对该图进一步的理解和总结 1. JS 对象概念的辨析 对象是什么:保存在堆中一块区域,同时在栈中有一块区域保存其在堆中的地址(也就是我们通常说的该变量指向谁&…...