解决线程安全的几个方法

线程安全:线程安全问题的发现与解决-CSDN博客

Java中所使用的并发机制依赖于JVM的实现和CPU的指令。 所以了解并掌握深入Java并发编程基础的前提知识是熟悉JVM的实现了解CPU的指令。

1.volatile简介

在多线程并发编程中,有两个重要的关键字:synchronized和volatile,译为

volatile是轻量级的synchronized,它在多线程开发中确保了共享内存变量的"可见性"。

什么叫做可见性?简要的概述其实很简单:

当一个线程修改一个共享变量的话,另一个线程能知道并读到这个修改后的值。

如果volatile变量修饰符使用得当的话,会比synchronized的使用和执行成本更低,因为它不会引起

线程的上下文切换和调度。

1.1volatile的定义与使用

volatile的定义:Java语言允许线程共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁更加方便。如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。

上面我们提到了,可见性,volatile,synchronized还有排他锁这几个新鲜的概念,我们来逐个讨论一下,等到了最后volatile关键字也就理解的差不多了。

1.内存可见性

谈到可见性,一般都是内存可见性,内存可见性的问题是由于编译器优化导致的,

正如我们开头展示的笔记那样,一个Java文件想要被cpu所执行,需要经历重重编译,转化等等

而在程序员这个圈子里,水平各个参差不齐,总的来说还是菜鸟更多,大佬更少,怎么样在这种情况让菜鸟也能写出来优秀的代码呢?大佬们就在编译器上动了手脚,加入了优化机制这样一来,即使初学者写出的代码不够高效,编译器也能在背后“兜底”,生成更高性能的执行代码,从而实现“写出来的代码比人本身更聪明”的目标。编译器编译的时候自动分析代码的逻辑,在保持代码逻辑不变的前提下,自动修改代码的内容,从而让代码变得更高效。

在这个案例中,我们希望通过线程2来控制线程1的循环条件,从而控制线程1的结束。

从线程1的循环中,我们可以看到,每次循环的条件,再假设线程2不能控制的前提下,都是为真的,编译器就发现了

1. 这里的isRunning每次读到的都是相同的值,仅仅1s足够让循环执行上万次,重复无效的代码了

2. 编译器查看循环条件和循环体并没有发现需要修改的地方

对于编译器而言,它无法静态分析出这个修改何时发生、是否会发生,甚至是否发生在同一个内存空间(因为线程间的可见性并不总是成立)

并且,这段代码的执行,依靠了,读内存操作,比较和跳转操作

通过读内存操作从内存中读取isRunning的值到CPU寄存器,通过比较寄存器存放的值和true是否相同,如果相同就继续执行否则使用跳转语句到指定的位置。

通过优化后变为

  • 从内存读取变量值:通过“load”操作,将共享变量 isRunning 的值从主内存读取到 CPU 寄存器 或说是线程工作内存中。

  • 寄存器中进行比较:循环条件判断时,CPU 不会每次都访问主内存,而是直接比较寄存器中或者说是工作内存中的值是否为 true

  • 分支跳转指令执行控制流

    • 如果等于 true,程序继续执行循环体;

    • 如果等于 false,程序跳转到循环之后的位置,退出循环

也就是说,编译器此处做了个大胆的决定,把访问内存这步操作在第一次访问后给优化掉了,后续的循环只需要从CPU寄存器或者缓存中读取值即可!

此时如果t2线程即使修改了isRunning的值,t1线程也无法感知到了,t1已经被优化了并没有从内存中读取而是从(寄存器/缓存)工作内存中读取了!

对于多线程中的内存可见性问题,其中一个关键原因就是编译器为了优化性能而对代码进行了重排和缓存

比如,在一个循环中重复读取某个变量的值,编译器会认为:

“这个变量的值在循环体内没有被修改,而且看上去始终相同,那我就没必要每次都从内存中去读了,直接缓存到寄存器里用就行。

小问题:如果我们此时将While循环中的空代码块加入Thread.sleep(1)后发现 

线程1居然神奇的受到了线程2输入的非零数字的影响结束了循环。

难道说Thread.sleep()也能解决内存可见性的问题吗?

我们知道,内存可见性的问题本质上是编译器优化所带来的,但是引入sleep后这个代码中的 ,从内存读取的操作并没有被编译器优化掉

代码的指令大致有

1.从内存中读取数据load

2.cmp通过比较来判断循环条件是否为真

3.sleep方法(背后是很多多的指令)

哪怕是sleep(0)在这里也不会被优化掉

我们在循环体中做各种复杂的操作,都会引起上述的优化失效!

综上内存可见性的问题,我们已经了解的差不多了,可以谈一下volatile关键字了,

如果我们在代码的isRunning变量加上了volatile关键字,就可以解决上述的问题!

 

有没有觉得很神奇,仅仅只是加了个关键字就解决了,我们谈论那么长时间的内存可见性问题?

总结成一句话来说:

volatile 保证可见性,靠的是底层 JIT 编译器在写操作中生成带 lock 前缀的汇编指令,这个指令通过缓存一致性协议,确保变量修改对所有 CPU 可见。

2.synchronized简介

请注意volatile关键字只能解决内存可见性的问题,对于,多个进程访问修改同一个变量,而造成的线程安全问题是无能为力的只能依靠synchronized

2.1synchronized的定义和使用

在多线程并发编程中,synchronized真是一位远古大能级别的角色,很多人会称呼他为重量级锁,

但是随着JavaSE的各种优化,有些情况下,他就不是那么重了。

synchronized实现同步的基础:Java中每一个对象都可以作为锁。具体表现为以下三种形式:

1.对于普通同步方法,锁是当前的实例对象

2.对于静态同步方法,锁是当前类的Class对象

3.对于同步方法块,锁是synchronized括号中配置的对象

当一个线程试图访问同步代码块时,他首先必须要先得到锁,退出或者抛出异常时必须释放锁。

synchronized(obj){...}中的obj就是在同步代码块中用来加锁的那种对象,JVM会对这个obj对象的监视器(monitor)进行加锁和解锁,从而实现线程之间的互斥。

注意:此处加锁并不是禁止线程调度,而是防止其他线程插队。

该锁块中一共有大概

count++ == >(count = count + 1)

load(从内存读取变量 count 的当前值)

add(对值进行+1的操作)

save(将新值写回内存)

三个指令操作,执行上述这些操作指令的时候,是随时会被其他线程插队从cpu上调度走的,如果加了锁就保证了操作的原子性,

因为此时如果其他线程尝试加锁操作,就会产生阻塞,从而避免执行上述指令时被插队的问题。

(使用lock和unlock来代替synchronized的{ 和} )

 synchronized的要点

1.进入 { 就是加锁,离开 } 就是解锁 

2.加锁操作是为了防止其他线程在本线程执行中插队,而不影响本线程调度

3.锁对象,两个或者多个线程针对同一个对象加锁才会有锁竞争,锁才会生效

对于下面的代码是否存在线程安全问题?

对于两个线程一个加锁,一个没有加锁是会产生线程安全的问题的,

因为在一把锁生效时,原子操作仍然会被打断,另一个线程并没有因为锁而受到限制

对于下面两种加锁的方式,就涉及到锁的粒度

t1线程:

对整个循环操做加锁,锁的粒度大,锁内部代码逻辑复杂

t2线程

每一次循环操作都会加锁,加100次锁,锁的粒度小,锁内部代码逻辑少

由于synchronized的设计

在synchronized(){

}代码块中,

Java 中的 synchronized 关键字由 JVM 保证:无论同步代码块中是正常执行、return 提前返回,还是抛出异常(throw)提前终止,都会自动执行解锁(unlock)操作。

这一点是很多高级语言设计lock和unlock操作的不足之处

1.可重入锁

对于下面的代码,是否可以正常运行呢?

假设说不存在可重入锁的概念,我们来分析

当线程2进入第一层锁,此时已经加锁成功,如果此时再对同一个对象加第二次锁就会产生死锁,因为第一次加锁的解锁操作需要等到第二次加锁并解决成功,而第二次的加锁操作又得等第一次解锁,就死锁了。

但是Java中存在可重入锁的概念,十分简单:

Java 中的 synchronized可重入锁(Reentrant Lock),其工作机制:

  • 每个锁记录:

    • 当前持有锁的线程ID

    • 当前线程对这把锁的重入次数(计数器)

于是:

  1. 线程 T1 首次获得锁 obj,线程ID 被记录,重入次数为 1。

  2. T1 再次进入 synchronized(obj),JVM 检查:锁的持有者仍是 T1,本线程重入,于是允许继续进入,同时 重入计数 +1

  3. 等两个 synchronized 代码块都执行完后,T1 每退出一层,重入计数 -1,直到为 0 时,才真正释放锁。

避免了“自己锁死自己”的问题,确保线程可以多次、安全地进入同一把锁控制的临界区。

那么锁到底存在哪里呢?锁里面会存储什么信息呢?

这些就涉及到深入的理解了

synchronized的实现原理与应用&Java对象的内存布局_java synchronized原理java对象内存布局-CSDN博客

3.wait和notify简介

3.1wait的定义和使用

首先需要清楚的是,wait和notify并不是Thread包括任何线程相关类的方法,而是Object基类的方法

在多线程的世界中,线程的调度是随机的,虽然join方法可以简单的控制线程的结束时间,

Thread.join() 是一个同步等待方法,可以让主线程等待子线程执行完毕之后再继续执行。

在main线程中调用t1,join()和t2.join(),main线程会等待t1和t2执行完毕,main才会执行完毕,而且t1和t2的执行完毕顺序也不确定

学习过操作系统课程的一定见过一个很经典的操作,叫PV操作,里面的代码都是手写的,需要我们来分析,等到线程1完成了什么什么条件或者任务就会唤醒线程2的操作等等,但是PV操作和我们的wait和notify操作有着本质的区别

1.PV操作是操作系统底层的操作叫原语,基于“信号量(Semaphore)”,通过计数控制资源访问

2.wait和notify方法是Java语言层面,基于“对象监视器(Monitor)”,通过条件变量进行线程协作

我们学到这里可以把PV操作暂时先忘掉了,虽然二者的很多用法相同,但是为了避免混淆还是不提及

程序中存在t1线程,t2线程

要求t1先执行某个逻辑A 然后t2再执行某个逻辑B

就比如我们生活的例子,只有A球员把球传给B球员,B球员才能完成扣篮的操作

虽然wait方法任何对象都可以直接调用,如果我们直接调用的话会抛出以下异常:

1.在使用前wait也和sleep方法一样需要抛出InterruptedException异常

2.运行后发现抛出了java.lang.IllegalMonitorStateException异常

翻译一下就是非法的监视器状态异常也就是说

你现在没有处于这个对象的监视器锁内部状态,却调用了必须在其中调用的方法。

JVM内部实现synchronized时,使用了形如monitor属性作为变量/方法名,也被称为“监视器锁

wait() 必须在 synchronized(obj) 中使用,否则 JVM 会抛出 IllegalMonitorStateException,因为你没有持有该对象的 monitor 锁。

就像你去面试一样,你都没有准备去面试呢,就在想以后薪资会给你开多少。

使用wait的时候如果没有被notify就会一直阻塞

在synchronized代码块中一共有三个动作:

1.释放掉当前锁

2.等待其他线程通知,此时处于阻塞状态

3.当通知到达后,从阻塞状态到就绪状态,并重新尝试获取到锁

假如wait一直占着锁,别的线程会一直等待锁,造成死锁

wait如果是无参版本的话,属于是死等,而wait也存在有参数的版本,同sleep一样,等待一定的时间就不会等待了,

同时notify也有一个notifyAll的版本会唤醒所有线程,而notify只是随机唤醒,上述例子中只有两个线程,所以一个wait一个唤醒,如果多个线程就不一定的。

sleep和wait的区别

sleep() 是让线程“暂停”一会儿,wait() 是让线程“等待”别人通知它继续。

对比点wait()sleep()
1. 设计目的主要用于线程间通信与协作,通常与 notify() / notifyAll() 搭配使用。主要用于让线程休眠一段时间,是一种简单的阻塞延迟机制。
2. 是否释放锁释放锁wait() 会释放当前对象的监视器锁(monitor)。不释放锁。线程进入休眠时仍然持有锁。
3. 是否需要在同步块中调用必须synchronized 块中使用,否则抛出 IllegalMonitorStateException不需要,可以在任何地方调用。
4. 是否可以被唤醒可以被 notify() / notifyAll() 唤醒,也可以被 interrupt() 打断。只能被 interrupt() 打断,无法被 notify() 唤醒。
5. 是否抛异常需要处理 InterruptedException也需要处理 InterruptedException
6. 唤醒后行为通常被唤醒后继续参与协作,如再次 wait() 或继续执行临界区。被打断或睡眠时间到后继续执行,不涉及线程间协作

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

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

相关文章

大模型应用班-第2课 DeepSeek使用与提示词工程课程重点 学习ollama 安装 用deepseek-r1:1.5b 分析PDF 内容

DeepSeek使用与提示词工程课程重点Homework:ollama 安装 用deepseek-r1:1.5b 分析PDF 内容python 代码建构:1.小模型 1.5b 可以在 笔记本上快速执行2.分析结果还不错3. 重点是提示词 prompt 的写法一、DeepSeek模型创新与特点1. DeepSeek-V3模型特点采用…

在FreeBSD系统下使用llama-cpp运行飞桨开源大模型Ernie4.5 0.3B(失败)

先上结论,截止到目前2025.7.25日,还不能用。也就是Ernie4.5模型无法在llama.cpp 和Ollama上进行推理,原因主要就llama是不支持Ernie4.5异构MoE架构。 不局限于FreeBSD系统,Windows也测试失败,理论上Ubuntu下也是不行。…

OpenCV图像梯度、边缘检测、轮廓绘制、凸包检测大合集

一、图像梯度 在图像处理中,「梯度(Gradient)」是一个非常基础但又极其重要的概念。它是图像边缘检测、特征提取、纹理分析等众多任务的核心。梯度的本质是在空间上描述像素灰度值变化的快慢和方向。 但我们如何在图像中计算梯度?…

GitHub 趋势日报 (2025年07月25日)

📊 由 TrendForge 系统生成 | 🌐 https://trendforge.devlive.org/ 🌐 本日报中的项目描述已自动翻译为中文 📈 今日获星趋势图 今日获星趋势图1814Resume-Matcher985neko714Qwen3-Coder622OpenBB542BillionMail486hrms219hyper…

编程语言Java——核心技术篇(五)IO流:数据洪流中的航道设计

🌟 你好,我是 励志成为糕手 ! 🌌 在代码的宇宙中,我是那个追逐优雅与性能的星际旅人。 ✨ 每一行代码都是我种下的星光,在逻辑的土壤里生长成璀璨的银河; 🛠️ 每一个算法都是我绘制…

基于FPGA的16QAM软解调+卷积编码Viterbi译码通信系统,包含帧同步,信道,误码统计,可设置SNR

目录 1.引言 2.算法仿真效果 3.算法涉及理论知识概要 3.1 16QAM调制软解调原理 3.2 帧同步 3.3 卷积编码,维特比译码 4.Verilog程序接口 5.参考文献 6.完整算法代码文件获得 1.引言 基于FPGA的16QAM软解调卷积编码Viterbi译码通信系统开发,包含帧同步,高斯…

Python数据分析基础(二)

一、Numpy 常用函数分类概览函数类别常用函数基本数学函数np.sum(x)、np.sqrt(x)、np.exp(x)、np.log(x)、np.sin(x)、np.abs(x)、np.power(a, b)、np.round(x, n) 等统计函数np.mean(x)、np.median(x)、np.std(x)、np.var(x)、np.min(x)、np.max(x)、np.percentile(x, q) 等比…

Colab中如何临时使用udocker(以MinIO为例)

本文主要是想记录一下自己在Colab中用udocker启动一个MinIO的容器的过程。 1. 命令行配置环境 由于目前没有用到GPU,所以我选择的是CPU的环境。(内存12G)然后就可以在命令行里安装udocker了,并配置minio的环境 # 由于minio需要做两个端口映射&#xff0c…

rt-thread 5.2.1 基于at-start-f437开发过程记录

基于rt-thread 5.2.1 bsp/at/at32f437-start进行开发,记录详细过程,包括中间遇到的各种坑。 at32f437-start原理图 自己设计的电路板主要换了一块小封装的同系列芯片, 目标是移植opENer。 1. 开发环境 env长时间不用,有点忘了。这次新下载…

EMCCD相机与电可调变焦透镜的同步控制系统设计与实现

EMCCD相机与电可调变焦透镜的同步控制系统设计与实现 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家,觉得好请收藏。点击跳转到网站。 摘要 本文详细介绍了基于Python的EMCCD相机&#xff0…

前缀和-560.和为k的子数组-力扣(LeetCode)

一、题目解析1.子数组是数组中元素的连续非空序列2.nums[i]范围为[-1000,1000],存在负数3.由于2的题目条件,该题不能用双指针算法,不具备单调性 二、算法原理解法1:暴力解法->枚举 O(N^2)固定一个值,向后枚举数组和…

解决企业微信收集表没有图片、文件组件,不能收集图片的问题

问题: 企业微信里面的收集表功能,有一个图片收集的收集表,但是插入的组件没有收集图片的组件? 原因: 大概率是微盘未启用 解决方法: 1、登陆企业微信管理后台 企业微信 2、访问微盘页面,…

认识单片机

《认识单片机》课程内容 一、课程导入 在我们的日常生活中,有很多看似普通却充满智慧的小物件。比如家里的智能电饭煲,它能精准地控制煮饭的时间和温度,让米饭煮得香喷喷的;还有楼道里的声控灯,当有人走过发出声音时&a…

数据结构(2)顺序表算法题

一、移除元素1、题目描述2、算法分析 思路1:查找val值对应的下标pos,执行删除pos位置数据的操作。该方法时间复杂度为O(n^2),因此不建议使用。思路2:创建新数组(空间大小与原数组一致&#xff0…

汽车电子架构

本文试图从Analog Devices官网中的汽车解决方案视角带读者构建起汽车电子的总体架构图,为国内热爱和从事汽车电子行业的伙伴们贡献一份力量。 一 、汽车电子架构总览 整个汽车电子包括四个部分:车身电子(Body Electronics)、座舱与…

pycharm 2025 专业版下载安装教程【附安装包】

安装之前,请确保已经关闭所有安全软件(如杀毒软件、防火墙等)安装包 👇链接:https://pan.xunlei.com/s/VOU-5_L1KOH5j3zDaaCh-Z28A1# 提取码:6bjy下载 PyCharm2025专业版 安装包 并 进行解压运行 pycharm-2…

在 Java 世界里让对象“旅行”:序列化与反序列化

Java 生态里关于 JSON 的序列化与反序列化(以下简称“序列化”)是一个久经考验的话题,却常因框架繁多、配置琐碎而让初学者望而却步。本文将围绕一段极简的 JsonUtils 工具类展开,以 FastJSON 与 Jackson 两大主流实现为例&#x…

High Speed SelectIO Wizard ip使用记录

本次实验的目的是通过VU9P开发板的6个TG接口,采用固定连接的方式,即X和X-维度互联,其框图如下所示:IP参数配置通过调用High Speed SelectIO Wizard来实现数据通路,High Speed SelectIO Wizard ip有24对数据通道&#x…

Execel文档批量替换标签实现方案

问题背景需求:俺现网班级作为维度,批量导出每个班级学员的数据,excel的个数在1k左右,每一张表的人数在90左右。导出总耗时在10小时左右。代码编写完成并导出现网数据后,发现导出的标题错了。解决方案1.通过修改代码&am…

SpringBoot配置多数据源多数据库

Springboot支持配置多数据源。默认情况,在yml文件中只会配置一个数据库。如果涉及到操作多个数据库的情况,在同实例中(即同一个ip地址下的不同数据库),可以采用数据库名点数据库表的方式,实现跨库表的操作。…