【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;使信息存…

【leetcode】常用数学题解法介绍

当涉及到ACM算法题中常见的数学常识和知识点时&#xff0c;以下是更加详细和全面的分析&#xff1a; 二进制&#xff1a; 二进制在计算机中是最基础的进制&#xff0c;它只包含两个数字0和1。在ACM算法题中&#xff0c;常用的二进制操作有&#xff1a; 位运算&#xff1a;包括…

关于三色标记算法

关于三色标记算法 三色标记算法是一种用于垃圾收集得算法&#xff0c;主要用于解决在并发垃圾收集中可能出现得对象引用更新问题。在JVM中&#xff0c;这种算法主要应用于CMS&#xff08;ConcurrentMarkSweep&#xff09;收集器和G1&#xff08;Garbage-first&#xff09;收集…

基于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…

【LeetCode+JavaGuide打卡】Day19|654.最大二叉树、617.合并二叉树、700.二叉搜索树中的搜索、98.验证二叉搜索树

学习目标&#xff1a; 654.最大二叉树 617.合并二叉树 700.二叉搜索树中的搜索 98.验证二叉搜索树 学习内容&#xff1a; 654.最大二叉树 题目链接&&文章讲解 给定一个不重复的整数数组 nums 。 最大二叉树 可以用下面的算法从 nums 递归地构建: 创建一个根节点&…

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

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

java基础 -- 事件监听器

要实现一个事件监听器&#xff0c;您可以使用Java中的事件模型和接口。以下是一个简单的示例&#xff0c;演示如何创建和使用一个事件监听器&#xff1a; 创建事件类&#xff08;Event Class&#xff09;&#xff1a; import java.util.EventObject;public class MyEvent ext…

ComfyUI添加IP白名单功能

AI生图很火&#xff0c;相信你对ComfyUI不陌生&#xff0c;查看ComfyUI的源码可以发现它是使用aiohttp来作为服务端的。那么我们在使用ComfyUI的时候可能需要做一些安全的限制&#xff0c;接下来我们将探讨如何在 ComfyUI 中添加 IP 白名单功能&#xff0c;以确保只有特定的用户…

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…

C#面:.NET中所有类型的基类是什么

System.Object 是C# .NET中所有类型的基类&#xff0c;它提供了一些通用的方法和属性&#xff0c;以及对象的类型信息和引用比较等功能。 例如&#xff1a;System.ObjectToString()&#xff0c;Equals()&#xff0c;GetHashCode() 等。 由于所有类型都继承自 System.Object&a…

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

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

图像预处理技术与算法

图像预处理是计算机视觉和图像处理中非常关键的第一步,其目的是为了提高后续算法对原始图像的识别、分析和理解能力。以下是一些主要的图像预处理技术: 1.图像增强: 对比度调整:通过直方图均衡化(Histogram Equalization)等方法改善图像整体或局部的对比度。 伽玛校正:…

MT4技术分析工具介绍:让你更好地把握市场趋势

在外汇交易市场中&#xff0c;技术分析是一种常用的分析手段&#xff0c;而MT4作为外汇交易中广泛使用的交易平台&#xff0c;拥有丰富的技术分析工具&#xff0c;能够帮助交易者更好地把握市场趋势。本文将介绍几款常用的MT4技术分析工具&#xff0c;帮助读者更好地理解和运用…

STM32 输入捕获模式测频率

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