【Java多线程】线程安全问题与解决方案

目录

1、线程安全问题

1.2、线程安全原因

2、线程加锁

2.1、synchronized 关键字

2.2、完善代码

2.3、对同一个线程的加锁操作 

3、内容补充

3.1、内存可见性问题 

3.2、指令重排序问题

3.3、解决方法

3.4、总结 volatile 关键字

1、线程安全问题

  • 某个代码,无论是单线程下执行还是多线程下执行都不会产生bug,被称之为“线程安全”
  • 如果在单线程下执行正确,但是多线程下会产生bug,被称之为“线程不安全”或者“存在线程安全问题”

线程安全问题的典型例子

public class ThreadDemo {private static int count = 0;public static void main(String[] args) throws InterruptedException {// 创建两个线程. 每个线程都针对上述 count 变量循环自增 50w 次Thread t1 = new Thread(() -> {for (int i = 0; i < 500000; i++) {count++;}});Thread t2 = new Thread(() -> {for (int i = 0; i < 500000; i++) {count++;}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = " + count);}
}

        按照正常逻辑来看这段代码,结果应该是100w,但是通过多次运行发现这里给的却是一个50w到100w的随机值, 这就是因为出现了线程安全问题导致的结果错误。

问题分析:

count++操作实际上分成三步:

1)load 从内存中读取数据到cpu的寄存器

2)add 把寄存器中的值+1

3)save 把寄存器的值写回内存中

        而由于线程调度是随机调度,抢占式执行的,这就导致了两个线程的count++操作三步骤是会被打乱顺序的。

        例如 t1 线程先执行到 1)的同时, t2 线程也刚好随机调度开始执行 1),导致t1 和 t2 读取到的数据都是0,此时 t1 线程对 寄存器中值0+1,并将 1 写回内存中,接着 t2 也执行相同操作,再次将 1 写回内存中。此时就出现了线程安全问题,两次count++却只让count自增了1次。这就是这段代码为什么不是100w的原因细节。

1.2、线程安全原因

透过两个线程分别对count++,可以看到线程的不安全,有以下原因
1、根本原因,操作系统上线程的调度策略是“随机调度,抢占式执行”的,这就给线程之间执行的顺序带来了很多的变数。是线程安全问题的“罪魁祸首”。
2、代码结构问题,代码中多个线程同时修改一个变量。
3、直接原因,上述多线程修改操作,本身不是“原子的”,即实际count++操作又被分成了三步操作。如果操作本身是“原子的”,那么它要么执行,要么不执行,就不会出现执行一半,就被调度走,让其他线程“可乘之机”。

  • 针对原因1,系统底层对调度线程的逻辑就是随机调度,抢占式执行,无法干预做出任何调整。
  • 针对原因2,代码结构问题有时候是需求决定的,并不是每次都可以从这里入手,因此对于原因2也不好调整。
  • 针对原因3,既然操作非“原子的”,那么可以通过一些特殊手段将其打包成为“整体”,这也是我们接下来要讲到的加锁

2、线程加锁

2.1、synchronized 关键字

其中 locker 可以是任意对象,进入 synchronized 修饰的代码块, 相当于加锁,退出 synchronized 修饰的代码块, 相当解锁。

如果一个线程,针对一个对象加上锁之后,其他线程也尝试对这个对象加锁,就会导致锁竞争进而引起阻塞(BLOCKED),这个阻塞会一直持续到上一个线程释放锁为止。
如果是两个线程分别针对不同的对象进行加锁,此时不会由锁竞争,也就不会阻塞。

可以形象的理解成,每个对象在内存中存储的时候,都存有一块内存表示当前的 "锁定" 状态(类似于厕所的 "有人/无人")。

如果当前是 "无人" 状态,那么就可以使用,使用时需要设为 "有人" 状态。

如果当前是 "有人" 状态,那么其他人无法使用,只能排队。

2.2、完善代码

public class ThreadDemo {private static int count = 0;public static void main(String[] args) throws InterruptedException {// 随便创建个对象都行Object locker = new Object();// 创建两个线程. 每个线程都针对上述 count 变量循环自增 50w 次Thread t1 = new Thread(() -> {for (int i = 0; i < 500000; i++) {synchronized (locker) {   //对count++进行加锁操作,打包三步为一步count++;}}});Thread t2 = new Thread(() -> {for (int i = 0; i < 500000; i++) {synchronized (locker) {   //对count++进行加锁操作,打包三步为一步count++;}}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = " + count);}
}

通过对count++的整体加锁,使得每一次的count++都是一个整体,解决了此处的线程安全问题。

2.3、对同一个线程的加锁操作 

public static void main(String[] args) {Object locker = new Object();Thread t = new Thread(() -> {synchronized (locker) {synchronized (locker) {System.out.println("hello synchronized");}}});t.start();
}

需要注意的是,这里最直观的感觉是进行了两次加锁,会发生锁冲突。第一次针对locker加锁之后,在还没释放锁的时候又尝试对locker加锁,理论会出现锁冲突,但是这里却可以正常打印。

最关键的问题在于,【Java中的锁是可重入锁】这两次加锁,其实是在同一个线程中进行的,如果是同一个线程对同一个锁的多次加锁,是不会冲突的。

3、内容补充

当然,还有其他能够导致出现线程安全问题的原因:内存可见性问题以及指令重排序问题

3.1、内存可见性问题 

下列代码原本用意是:当用户输入非0数字时,结束线程t1。 

package thread;import java.util.Scanner;public class ThreadDemo {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();}
}

实际运行结果时发现无法结束,t2修改了内存,但是t1内有看到这个内存的变化,就称之为“内存可见性”问题。出现这一问题是JVM的代码优化导致的。

t1 线程中的while语句每次循环是都有两个操作:

1、load 读取内存中 flag 的值到 cpu 寄存器中

2、拿到寄存器的值和 0 比较

上述循环中循环的执行速度非常之快,反复的执行1和2,即使是1秒也可能反复执行了几百万次。而在执行的过程中,有两个关键要点:

1、JVM识别到 load 操作执行的几百万次结果每次都一样(输入前的等待时间里)。

2、而由于 load 操作花费的开销远远超过剩余的其他操作(访问寄存器的操作速度远远超过访问内存)

每次循环可能就是百分之九十九的时间都消耗在 load 操作上,而百分之一的时间消耗在其他操作上,而且JVM发现每次 load 操作读取到的数据都是一样的,那么此时JVM就会认为此处每次 load 的操作是否有存在的必要呢,于是乎JVM就可能会自动执行了代码优化,将上述的 load 操作优化了(只有前几次进行了 load,后续发现 load 一直没有变化,分析代码也没发现哪里修改了flag,因此激进的将load操作优化成了直接使用寄存器中之前“缓存”的值),从而达到大幅度提高循环的执行速度的目的。

3.2、指令重排序问题

指令重排序也是编译器优化的一种方式。保证逻辑不变的前提下,调整原有代码的执行顺序,提高程序的效率。

3.3、解决方法

由于上述两种问题都是由于JVM代码优化导致的

Java提供的 volatile 关键字就可以使上述的优化被强制关闭,可以确保每次循环条件都会重新从内存中读取数据。
强制读取内存,虽然开销大了,效率也低了,但是数据的准确性、逻辑的准确性都提高了。

只需要对 flag 添加一个 volatile 关键字即可解决这一问题。

3.4、总结 volatile 关键字

1、保证内存可见性,每次访问变量必须都要重新读取内存,而不会优化到寄存器/缓存中
2、禁止指令重排序,针对被 volatile 修饰的变量的读写操作相关指令,是不能被重排序的。

【博主推荐】

【Java多线程】Thread类的基本用法-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/zzzzzhxxx/article/details/136121421?spm=1001.2014.3001.5501 【Java多线程】对进程与线程的理解-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/zzzzzhxxx/article/details/136115808?spm=1001.2014.3001.5501

【数据结构】二叉树的三种遍历(非递归讲解)-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/zzzzzhxxx/article/details/136044643?spm=1001.2014.3001.5501

如果觉得作者写的不错,求给博主一个大大的点赞支持一下,你们的支持是我更新的最大动力!

如果觉得作者写的不错,求给博主一个大大的点赞支持一下,你们的支持是我更新的最大动力!

如果觉得作者写的不错,求给博主一个大大的点赞支持一下,你们的支持是我更新的最大动力!

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

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

相关文章

初识结构体(C语言)

目录 1、结构体声明 2、结构体访问 3、结构体传参 1、结构体声明 结构是一些值的集合&#xff0c;这些值称为成员变量。结构的每一个成员可以是不同类型的变量。有点像数组&#xff0c;但是一个数组只能存放同一种类型的变量。如果要描述复杂对象的时候&#xff0c;对象由多…

基于Java SSM框架实现留学生交流互动论坛网站项目【项目源码+论文说明】

摘要 21世纪的今天&#xff0c;随着社会的不断发展与进步&#xff0c;人们对于信息科学化的认识&#xff0c;已由低层次向高层次发展&#xff0c;由原来的感性认识向理性认识提高&#xff0c;管理工作的重要性已逐渐被人们所认识&#xff0c;科学化的管理&#xff0c;使信息存…

基于ant的图片上传组件封装(复制即可使用)

/*** 上传图片组件* param imgSize 图片大小限制* param data 上传数据* param disabled 是否禁用*/import React, { useState,useEffect } from react; import { Upload, Icon, message} from antd; const UploadImage ({imgSize 50,data { Directory: Image },disabled f…

Vue封装全局公共方法

有的时候,我们需要在多个组件里调用一个公共方法,这样我们就能将这个方法封装成全局的公共方法。 我们先在src下的assets里新建一个js文件夹,然后建一个common.js的文件,如下图所示: 然后在common.js里写我们的公共方法,比如这里我们写了一个testLink的方法,然后在main…

Apache Flink连载(三十):Flink 内存模型

🏡 个人主页:IT贫道-CSDN博客 🚩 私聊博主:私聊博主加WX好友,获取更多资料哦~ 🔔 博主个人B栈地址:豹哥教你学编程的个人空间-豹哥教你学编程个人主页-哔哩哔哩视频 目录

【GUI编程】Tkinter之OptionMenu

OptionMenu OptionMenu类是一个辅助类&#xff0c;它用来创建弹出菜单&#xff0c;并且有一恶搞按钮显示它。它非常类似Windows上的下拉列表插件。 如果要获取当前选项菜单的值&#xff0c;你需要把它和一个Tkinter变量联系起来。 def __init__(self, master, variable, val…

“无限交互,全新驾驶体验!智能语音小车,与您共同开创未来出行。”#51单片机最终项目《智能语音小车》【上】

"无限交互&#xff0c;全新驾驶体验&#xff01;智能语音小车&#xff0c;与您共同开创未来出行。”#51单片机最终项目《智能语音小车》【上】 前言预备知识1. L9110S电机控制器接线1.1 L9110S概述1.2 L9110S IO口描述1.3 L9110S 实物图1.4 L9110S与单片机接线 2. L9110前…

PostgreSQL按日期列创建分区表

在PostgreSQL中&#xff0c;实现自动创建分区表主要依赖于表的分区功能&#xff0c;这一功能从PostgreSQL 10开始引入。分区表可以帮助管理大量数据&#xff0c;通过分布数据到不同的分区来提高查询效率和数据维护的便捷性。以下是在PostgreSQL中自动创建分区表的一般步骤&…

【Git】Gitbash使用ssh 上传本地项目到github

SSH Git上传项目到GitHub&#xff08;图文&#xff09;_git ssh上传github-CSDN博客 前提 ssh-keygen -t rsa -C “自己的github电子邮箱” 生成密钥&#xff0c;公钥保存到自己的github的ssh里 1.先创建一个仓库&#xff0c;复制ssh地址 git init git add . git commit -m …

GEE必须会教程—跳舞的线(字符串类型)

字符串&#xff0c;GEE上跳舞的线&#xff01; GEE学习之路漫长&#xff0c;跟着小编一起走进今天的数据类型的学习。字符串是各大编程语言的常用数据类型&#xff0c;我们今天需要了解GEE平台上字符串的定义、以及常用的方法。 1.定义字符串 //字符串构造 var base_str &q…

「Java同步原理与底层实现解析」

原理概要&#xff1a; java虚拟机中的同步基于进入与结束Monitor对象实现&#xff0c;无论是显式同步&#xff08;同步代码块进入在jvm是根据monitorenter标志、结束是monitorexit标志&#xff0c;那最后一个是monitorexit是异常结束时被执行的释放指令&#xff09;、隐式同步…

STM32 输入捕获模式测频率

单片机学习&#xff01; 目录 文章目录 前言 一、输入捕获测频率配置步骤 二、代码示例及注意事项 2.1 RCC开启时钟 2.2 GPIO初始化 2.3 配置时基单元 2.4 配置输入捕获单元 2.5 选择从模式的触发源 2.6 配置从模式为Reset 2.7 开启定时器 总结 前言 博文介绍如何配置输入捕获电…

OpenAI 全新发布文生视频模型 Sora,支持 60s 超长长度,有哪些突破?将带来哪些影响?

Sora大模型简介 OpenAI 的官方解释了在视频数据基础上进行大规模训练生成模型的方法。 我们下面会摘取其中的关键部分罗列让大家快速get重点。 喜欢钻研的伙伴可以到官网查看技术报告&#xff1a; https://openai.com/research/video-generation-models-as-world-simulator…

AI破局俱乐部,你要了解的都在这里

您好&#xff0c;我是码农飞哥&#xff08;wei158556&#xff09;&#xff0c;感谢您阅读本文&#xff0c;欢迎一键三连哦。&#x1f4aa;&#x1f3fb; 1. Python基础专栏&#xff0c;基础知识一网打尽&#xff0c;9.9元买不了吃亏&#xff0c;买不了上当。 Python从入门到精通…

【C#】使用代码实现龙年春晚扑克牌魔术(守岁共此时),代码实现篇

欢迎来到《小5讲堂》 大家好&#xff0c;我是全栈小5。 这是《C#》系列文章&#xff0c;每篇文章将以博主理解的角度展开讲解&#xff0c; 特别是针对知识点的概念进行叙说&#xff0c;大部分文章将会对这些概念进行实际例子验证&#xff0c;以此达到加深对知识点的理解和掌握。…

大模型量化技术原理-LLM.int8()、GPTQ

近年来&#xff0c;随着Transformer、MOE架构的提出&#xff0c;使得深度学习模型轻松突破上万亿规模参数&#xff0c;从而导致模型变得越来越大&#xff0c;因此&#xff0c;我们需要一些大模型压缩技术来降低模型部署的成本&#xff0c;并提升模型的推理性能。 模型压缩主要分…

不知如何获取1688工厂档案信息,你还在为此烦恼吗?

阿里巴巴集团旗下的B2B电子商务网站&#xff0c;提供海量优质商品&#xff0c;为采购商和供应商提供交流、合作、采购等服务&#xff0c;是很多没有货源优势的电商卖家首选的货源途径&#xff0c;也是国内最大、货源种类最齐全的货源网站。 不少做跨境电商无货源的朋友都想要1…

用html编写的招聘简历

用html编写的招聘简历 相关代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</tit…

自定义异常处理演示

​ 为了防止黑客从前台异常信息&#xff0c;对系统进行攻击。同时&#xff0c;为了提高用户体验&#xff0c;我们都会都抛出的异常进行拦截处理。 一、全局异常处理 编写一个异常拦截类&#xff0c;如下&#xff1a;ControllerAdvice&#xff0c;很多初学者可能都没有听说过…

《最新出炉》系列初窥篇-Python+Playwright自动化测试-19-处理鼠标拖拽-中篇

1.简介 上一篇中&#xff0c;主要是介绍了拖拽的各种方法的理论知识以及实践&#xff0c;今天宏哥讲解和分享一下划取字段操作。例如&#xff1a;需要在一堆log字符中随机划取一段文字&#xff0c;然后右键选择摘取功能。 2.划取字段操作 划取字段操作就是在一段文字中随机选…