Java【多线程基础3】 多线程有风险! 为啥线程不安全? 如何解决?
创始人
2025-05-31 09:38:07
0

文章目录

  • 前言
  • 一、线程不安全的原因
    • 1, 多线程调度的随机性(抢占式执行)
    • 2, 原子性
    • 3, 内存可见性
      • 3.1, Java 内存模型(JMM)
    • 4, 指令重排序
  • 二、线程不安全的示例1
    • 1, 代码示例
    • 2, 原因分析
  • 三、snychronized 关键字
  • 四、线程不安全的示例2
    • 1, 代码示例
    • 2, 原因分析
  • 五、volatile 关键字
  • 总结


前言

各位读者好, 我是小陈, 这是我的个人主页
小陈还在持续努力学习编程, 努力通过博客输出所学知识
如果本篇对你有帮助, 烦请点赞关注支持一波, 感激不尽
希望我的专栏能够帮助到你:
JavaSE基础: 从数据类型类和对象, 封装继承多态, 接口, 综合小练习图书管理系统
Java数据结构: 顺序表, 链表, 二叉树, , 哈希表等 (正在持续更新)
JavaEE初阶: 多线程, 网络编程, html, css, js, severlet, http协议, linux等(正在持续更新)

上篇[多线程基础2]主要介绍了 : Thread类 的构造方法, 常用成员属性, 常用成员方法以及多线程的状态, 状态转换
本篇继续介绍多线程相关的基础内容, 内容较多, 分为若干篇持续分享


提示:是正在努力进步的小菜鸟一只,如有大佬发现文章欠佳之处欢迎批评指点~ 废话不多说,直接上干货!

一、线程不安全的原因

如何定义线程是否安全?

如果多线程环境的代码执行结果, 和单线程环境的代码执行结果一致, 则认为线程是安全的, 否则认为线程不安全

下面介绍线程不安全的几个原因


1, 多线程调度的随机性(抢占式执行)

这是导致多线程环境下 线程不安全 的最根本原因

由于多个线程是 “抢占式执行的” , 所以造成了多线程调度的随机性, 无序性

多线程在 CPU 上并发执行, 而 CPU 只能看懂二进制的指令, 所以多线程调度时的随机性, 无序性, 就有可能造成这些指令的混乱

所以多个线程互相影响起来, 也是无迹可寻的


2, 原子性

原子性是指 : 不可分割的最小单位, CPU 执行的一条指令, 就是满足原子性的

然而一行 Java 代码(即便很简单易懂), 也不一定满足原子性, 因为这一行代码可能分为很多条指令

如果不满足原子性, 在多线程环境下, CPU 正在执行线程 A 的代码对应的指令, 此时另一个线程过来插了一脚, CPU 去执行线程 B 的代码对应的指令, 整个程序就有可能发生错误


3, 内存可见性

可见性指 : 一个线程对共用变量的修改, 能够及时地被其他线程看到

如果不满足内存可见性, 在多线程环境下, 线程 A 修改了某个共用变量的值, 线程 B 看不到这这共用变量被修改了, 还在使用修改前的值, 程序就有可能发生错误

哎? 为啥线程 A 修改了共用变量, 线程 B 不能及时看到呢? 这就要谈谈 Java 内存模型


3.1, Java 内存模型(JMM)

在这里插入图片描述

线程之间的共用变量存在 主内存 (Main Memory)
每一个线程都有自己的 “工作内存” (Working Memory)
当线程要读取一个共用变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据
当线程要修改一个共用变量的时候, 也会先修改工作内存中的副本, 再同步回主内存

这里的主内存才是平常说的内存, 工作内存 其实是 寄存器 高速缓存

不满足内存可见性的情况 :
由于每个线程有自己的工作内存, 这些工作内存中的内容相当于同一个共享变量的 “副本”. 此时修改线程A 的工作内存中的值, 线程B 的工作内存不一定会及时变化

为什么Java官方要把 寄存器和高速缓存, 定义成一个新的术语"工作内存"?
因为早期的CPU中是没有高速缓存的, 并且由于Java的可移植性, 为了应付不同电脑上的硬件软件差异, 保证文档的规范性, 适用性, 定义了"工作内存"


4, 指令重排序

例如 : 如下有四条指令
1, 我从宿舍出发
2, 要去食堂吃饭
3, 要去快递站拿快递
4, 要帮舍友带饭

按照1 --> 2 --> 3 --> 4 的顺序执行, 我的路线是这样的 :
在这里插入图片描述

把 3 , 4 互换位置后, 按照1 --> 2 --> 4 --> 3 的顺序执行, 我的路线是这样的 :
在这里插入图片描述
保证逻辑不变 的前提下, 更改多条指令的顺序, 从而提高程序执行效率, 这就是指令重排序

有些指令重排序能够提高执行效率 有时不能

这一点在单线程环境下比较容易判断, 但是在多线程环境下就没那么容易了, 多线程的代码执行复杂程度更高, 编译器很难在编译阶段对代码的执行效果进行预测, 因此激进的重排序很容易导致优化后的逻辑和之前不等价, 这是指令重排序可能造成的弊端

并且重排序是一个比较复杂的话题, 涉及到 CPU 以及编译器的一些底层工作原理, 此处不做过多讨论, 了解即可


二、线程不安全的示例1

1, 代码示例

首先定义一个 Counter类

class Counter {private int number = 0;public void add(){number++;}public int getCount(){return number;}
}

我们创建两个线程,两个线程都调用 add方法 5k 次,两个线程结束后主线程中获取 number 的值

public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Thread thread1 = new Thread( () -> {for (int i = 0; i < 5000; i++) {counter.add();}});Thread thread2 = new Thread( () -> {for (int i = 0; i < 5000; i++) {counter.add();}});thread1.start();thread2.start();thread1.join();thread2.join();System.out.println(counter.getCount());}

预期结果 : 最终number的值是1w,来看运行结果
在这里插入图片描述
实际结果 : 并不是 1w, 而是小于1w, 并且多次运行的执行结果都不一样
在这里插入图片描述
在这里插入图片描述


2, 原因分析

原因就在于 add方法 调用后的 number++ 这个操作并不是原子的
这一行代码看似简单易懂,实际上是三条 CPU 指令 :
1, load 从内存中读取 number 的值, 到寄存器中
2, add 修改寄存器中 number 的值, 把 number + 1
3, save 把寄存器中的值写回到内存中
其实就是上述的 Java内存模型 的机制

两个线程并发执行时, 如果是 理想情况 :
在这里插入图片描述
这三条指令没有相互交错, 就不会对最后的值产生影响, 不妨就称为 理性情况

对应的, 如果 thread1线程 在 CPU 上执行到 load 指令时, 读到 number 的值为 1 , 本该继续执行后两条指令, 但是突然被 thread2线程 抢占执行了, CPU 开始执行thread2线程 的三条指令, 就是 非理想情况 :
**加粗样式**
只要是 number++ 时, 两个线程发生了 “抢占式” 执行, 导致了有一次修改无效(被覆盖), 不妨就称为 非理性情况

正是因为 number++ 这个操作不是原子的, 所以才会在 “抢占式” 执行时产生问题, 可是, 只要是多线程环境, 就无法改变 “抢占式” 执行这一机制, 那有没有一种可能, 我们把 number++ 变成原子性的呢?

当然可以, 就是通过 “加锁” 来实现, 即 : 使用 snychronized 关键字


三、snychronized 关键字

只需要在 Counter类 中的 add方法上, 加一个 snychronized 关键字

    synchronized public void add(){number++;}

来看运行结果 :
在这里插入图片描述
符合上面示例的预期结果

snychronized 关键字 最主要的特性就是 : 互斥
例如 : 线程 A 执行到对象 Counter 的 synchronized 修饰的代码块中时, 线程 B 如果也同时执行到对象 Counter 的 synchronized 修饰的代码块, 线程 B 就会阻塞等待
进入 synchronized 修饰的代码块, 相当于 加锁
退出 synchronized 修饰的代码块, 相当于 解锁

其实很好理解 :
张三去上厕所, 进去之后锁门 (加锁) , 此时李四也想上厕所, 他就得在门口憋着, 等张三出来 (解锁) 之后, 才能进去厕所, 锁门 (加锁)
可如果在张三还没出来的时候, 李四和王五都想上厕所, 那么等张三出来 (解锁) 之后, 李四和王五就要抢这个厕所(锁竞争), 谁抢到谁就进去厕所, 锁门(加锁) ,

李四和王五 的过程就是多线程的抢占式执行

厕所, 其实就是一个锁对象

“锁” 这个话题很丰富, 以后还会详细介绍


四、线程不安全的示例2

1, 代码示例

定于一个成员属性 n 初始化为 0 , 再创建两个线程
在第一个线程中, 如果 n 为 0 , 一直循环, 没有循环体, 在第二个线程中, 从控制台输入整数, 赋值给 n

	public static int n = 0;public static void main(String[] args) {Thread thread1 = new Thread( () -> {while (n == 0) {}System.out.println(" n 不是 0 了, 循环结束");});Thread thread2 = new Thread( () -> {Scanner scanner = new Scanner(System.in);n = scanner.nextInt();});System.out.print("请输入一个整数 : ");thread1.start();thread2.start();

预期结果 : 如果输入的 n 不是 0 , thread1 就会退出循环, 执行打印语句
在这里插入图片描述

实际结果 : 并没有退出循环, 但 n 的值确实被修改了


2, 原因分析

既然 n 的值确实被修改了, 那么 thread1线程 中的循环没有退出的原因只能是, thread1线程 没有读取到修改过的 n 的值, 而是一直读取原本的 n 的值

因为在 while 循环中, 没有循环体, 在整个循环中只有两条指令
1, load 从内存中读取 n 的值到寄存器
2, 从寄存器中读取 n , 比较 n 的值和 0 是否相同

由于从寄存器中读取数据, 比从内存中读取数据快了 3 ~ 4 个数量级( 1k ~ 1w 倍), 所以对于这一整个循环来说, 执行 1次 指令1, 就可以执行 1k ~ 1w次 指令2

那么从内存中读取 n 的值就成了 “负担” , 所以编译器就进行了优化, 直接省去了指令1, 那么 thread1线程 中, n 的值就永远为 0

这就导致了上述实例中, 对于 n 这个共用变量, 在 thread1线程 和 thread2线程 中不满足 内存可见性, 如何解决这个问题呢? 使用 volatile关键字


五、volatile 关键字

只需要在共用变量 n 之前加上 volatile关键字 即可

    volatile  public static int n = 0;

volatile 修饰的变量, 能够保证 内存可见性, 能够 强制执行内存和寄存器之间的读写指令, 虽然导致速度慢了, 但是数据变的更准确了

volatile 还可以禁止指令重排序

但是 volatile 不保证原子性, 如果把 示例1 中的 synchronized 关键字 改成 volatile 关键字, 最终执行结果仍然不符合预期

所以, volatile 关键字 适合于一个线程读, 一个线程写的情况


总结

以上就是本篇的全部内容, 主要介绍了

如果本篇对你有帮助,请点赞收藏支持一下,小手一抖就是对作者莫大的鼓励啦😋😋😋~


上山总比下山辛苦
下篇文章见

相关内容

热门资讯

王凤英入职小鹏3年终获股权,此... 5月7日消息,小鹏汽车披露的监管及年报信息显示,公司总裁王凤英已正式进入股东名册,入职小鹏3年后股权...
五块钱红酒卖断货,便宜红酒为何... 最近一段时间,中国的酒类消费市场可以说是显得格外奇怪,一方面,各种高端酒特别是白酒的消费量出现了明显...
财联社C50风向指数调查:4月... 财联社5月8日讯(记者 夏淑媛)新一期财联社“C50风向指数”结果显示,市场机构对4月新增人民币贷款...
央视硬刚国际足联拒掏20亿,背... 作者| 史大郎&猫哥 来源| 是史大郎&大猫财经Pro 央视这次太刚了,离世界杯开幕还有1个月,死活...
新CEO上任直接放大招!Air... 快科技5月8日消息,苹果即将上任的CEO John Ternus对未来一系列新产品充满信心,称这些设...
“特朗普拟邀英伟达、波音等CE... 据路透社当地时间5月7日报道,特朗普政府正邀请英伟达、苹果、埃克森美孚、波音等大公司首席执行官,于下...
世界杯,还能看到直播吗? 2026年美加墨世界杯距离开幕,仅剩一个多月时间。多方信息显示,中央广播电视总台(以下简称“央视”)...
机构警告AI芯片热潮风险,超威... 5月7日,据央视财经,隔夜超威半导体公司(AMD)股价飙升近19%,带动AI芯片热潮持续升温。AMD...
银行员工转走储户1800万最新... 银行员工转走储户1800万最新进展:2名储户已收到银行全部款项
原创 中... 1994年,安徽省的经济格局曾发生过一次戏剧性的转折。在那一年,一座名为安庆的城市,其国内生产总值(...
昆都仑区:政策“蓄力”消费焕新 “一台5000多元的空调,叠加‘国补’和商场的以旧换新活动,能优惠1000元左右,旧机还能免费上门拆...
乐悦置业竞得佛山顺德乐从镇一商... 观点网讯:5月6日,佛山市顺德区乐从镇一商业地块成功出让,由广东省乐悦置业有限公司竞得,乐从南区·邻...
原创 亦... 《爱情没有神话》这部剧,一开始的命运颇为多舛,经历了几次撤档的波折后,终于在观众面前亮相,但其首播的...
美联储34年最大分歧叠加油价飙... 美联储按预期维持利率不变,但内部出现34年来最严重分歧,叠加布油创2022年6月以来新高,美债遭抛售...
支付宝消费券回收后,资金是否支... 摘要: 支付宝消费券回收变现后,资金能否直接转入信用卡?本文解答到账方式的相关规则,帮助用户了解资金...
中医介绍5个化痰穴位!收藏这篇... 很多人忽略了“痰”的危害,觉得咳几下就没事,殊不知,肺里的痰长期堆积,只会一步步加重身体负担。 中医...
黄金平台“杰我睿”涉嫌经济犯罪... 红星资本局5月7日消息,深圳水贝知名金店“杰我睿”兑付困难事件有了新进展。日前,深圳市公安局罗湖分局...
多地出台购房新政促楼市升温 记... 今年的“五一”假期,伴随着多个城市楼市新政密集落地,在叠加市场信心持续修复的作用下,房地产市场热度持...
谁是五一“吸金王”?这5座城市... 来源:市场资讯 (来源:21城市观) 哪座城市成为“五一”假期的大赢家? 图源:摄图网 作者|赵晓...
“低招低裁”格局稳固劳动力市场... 智通财经APP获悉,美国上周初请失业金人数在经历前一周回落至近几十年来最低水平后出现小幅反弹,表明尽...