JavaEE初阶——《多线程--. 多线程带来的的⻛险-线程安全 (重点)》

一、观察线程不安全

java 复制代码
// 此处定义⼀个 int 类型的变量
private static int count = 0;
public static void main(String[] args) throws InterruptedException {

 Thread t1 = new Thread(() -> {

 // 对 count 变量进⾏⾃增 5w 次
 for (int i = 0; i < 50000; i++) {
 count++;
 }
 });

 Thread t2 = new Thread(() -> {

 // 对 count 变量进⾏⾃增 5w 次
 for (int i = 0; i < 50000; i++) {
 count++;
 }
 });

 t1.start();
 t2.start();

 // 如果没有这俩 join, 肯定不⾏的. 线程还没⾃增完, 就开始打印了. 很可能打印出来的count 就是个 0
 t1.join();
 t2.join();
 // 预期结果应该是 10w
 System.out.println("count: " + count);
}

不安全不稳定输出的结果

t1和t2的join谁在前谁在后都没事

⼤家观察下是否适⽤多线程的现象是否⼀致?同时尝试思考下为什么会有这样的现象发⽣呢?

二、线程安全的概念

想给出⼀个线程安全的确切定义是复杂的,但我们可以这样认为:
如果多线程环境下代码运⾏的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。

三、线程不安全的原因

1.线程调度是随机的

这是线程安全问题的罪魁祸⾸
随机调度使⼀个程序在多线程环境下, 执⾏顺序存在很多的变数.
程序猿必须保证 在任意执⾏顺序下 , 代码都能正常⼯作.
修改共享数据
多个线程修改同⼀个变量
上⾯的线程不安全的代码中, 涉及到多个线程针对 count 变量进⾏修改.
此时这个 count 是⼀个多个线程都能访问到的 "共享数据"

2.原⼦性


什么是原⼦性

Java EE 原子性(Atomicity) 是事务 ACID 四大特性之一,指一组操作要么全部成功执行并提交,要么全部失败并回滚,绝不允许部分执行

原子性强调事务是不可分割的最小执行单元

  • 事务内所有操作(如数据库增删改、服务调用)必须整体成功,才算完成。
  • 任何一步失败,整个事务立即回滚,数据恢复到事务开始前的状态。
  • 不存在 "执行一半、中间状态暴露" 的情况。

比如:

我们把⼀段代码想象成⼀个房间,每个线程就是要进⼊这个房间的⼈。如果没有任何机制保证,A进⼊房间之后,还没有出来;B 是不是也可以进⼊房间,打断 A 在房间⾥的隐私。这个就是不具备原⼦性的。

那我们应该如何解决这个问题呢?是不是只要给房间加⼀把锁,A 进去就把⻔锁上,其他⼈是不是就进不来了。这样就保证了这段代码的原⼦性了。

有时也把这个现象叫做同步互斥 ,表⽰操作是互相排斥的。

⼀条 java 语句不⼀定是原⼦的,也不⼀定只是⼀条指令

⽐如刚才我们看到的 n++,其实是由三步操作组成的:

  1. 从内存把数据读到 CPU

  2. 进⾏数据更新

  3. 把数据写回到 CPU

不保证原⼦性会给多线程带来什么问题

如果⼀个线程正在对⼀个变量操作,中途其他线程插⼊进来了,如果这个操作被打断了,结果就可能是错误的。

一句话总结

Java EE 原子性 = 事务内操作 "要么全做,要么全不做",确保数据不会因部分执行而错乱。

这点也和线程的抢占式调度密切相关. 如果线程不是 "抢占" 的, 就算没有原⼦性, 也问题不⼤.

3.可⻅性

可⻅性指, ⼀个线程对共享变量值的修改,能够及时地被其他线程看到.

Java 内存模型 (JMM): Java虚拟机规范中定义了Java内存模型.

⽬的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到⼀致的并发效果

线程之间的共享变量存在 主内存 (Main Memory).

每⼀个线程都有⾃⼰的 "⼯作内存" (Working Memory) .

当线程要读取⼀个共享变量的时候, 会先把变量从主内存拷⻉到⼯作内存 , 再从**⼯作内存读取数据**.

当线程要修改⼀个共享变量的时候, 也会先修改**⼯作内存中的副本, 再同步回主内存**.

由于每个线程有⾃⼰的⼯作内存, 这些⼯作内存中的内容相当于同⼀个共享变量的 "副本". 此时修改线程1 的⼯作内存中的值, 线程2 的⼯作内存不⼀定会及时变化

初始情况下, 两个线程的⼯作内存内容⼀致.

  1. ⼀旦线程1 修改了 a 的值, 此时主内存不⼀定能及时同步. 对应的线程2 的⼯作内存的 a 的值也不⼀定能及时同步.

这个时候代码中就容易出现问题.

此时引⼊了两个问题:

1.为啥要整这么多内存?

2.为啥要这么⿇烦的拷来拷去?

1.为啥整这么多内存?

实际并没有这么多 "内存". 这只是 Java 规范中的⼀个术语, 是属于 "抽象" 的叫法.

所谓的 "主内存" 才是真正硬件⻆度的 "内存". ⽽所谓的**"⼯作内存", 则是指 CPU 的寄存器和⾼速缓存.**

2.为啥要这么⿇烦的拷来拷去?

因为 CPU 访问⾃⾝寄存器的速度以及⾼速缓存的速度, 远远超过访问内存的速度(快了 3 - 4 个数量级,也就是⼏千倍, 上万倍).

⽐如某个代码中要连续 10 次读取某个变量的值, 如果 10 次都从内存读, 速度是很慢的. 但是如果只是第⼀次从内存读, 读到的结果缓存到 CPU 的某个寄存器中, 那么后 9 次读数据就不必直接问内存了.
效率就⼤⼤提⾼了.
那么接下来问题⼜来了, 既然访问寄存器速度这么快, 还要内存⼲啥??
答案就是⼀个字: 贵

值的⼀提的是, 快和慢都是相对的. CPU 访问寄存器速度远远快于内存, 但是内存的访问速度⼜远远快于硬盘.
对应的, CPU 的价格最贵, 内存次之, 硬盘最便宜

4.指令重排序

什么是代码重排序

⼀段代码是这样的:

  1. 去前台取下 U 盘

  2. 去教室写 10 分钟作业

  3. 去前台取下快递

如果是在单线程情况下,JVM、CPU指令集会对其进⾏优化,⽐如,按 1->3->2的⽅式执⾏,也是没问题,可以少跑⼀次前台。这种叫做指令重排序
编译器对于指令重排序的前提是 "保持逻辑不发⽣变化". 这⼀点在单线程环境下⽐较容易判断, 但是在多线程环境下就没那么容易了, 多线程的代码执⾏复杂程度更⾼, 编译器很难在编译阶段对代码的执⾏效果进⾏预测, 因此激进的重排序很容易导致优化后的逻辑和之前不等价
重排序是⼀个⽐较复杂的话题, 涉及到 CPU 以及编译器的⼀些底层⼯作原理, 此处不做过多讨论

四、解决之前的线程不安全问题

java 复制代码
// 此处定义⼀个 int 类型的变量
private static int count = 0;
public static void main(String[] args) throws InterruptedException {

 Object locker = new Object();
 Thread t1 = new Thread(() -> {

 // 对 count 变量进⾏⾃增 5w 次
 for (int i = 0; i < 50000; i++) {
 synchronized (locker) {
 count++;
 }
 }
 });

 Thread t2 = new Thread(() -> {

 // 对 count 变量进⾏⾃增 5w 次
 for (int i = 0; i < 50000; i++) {
 synchronized (locker) {
 count++;
 }
 }
 });

 t1.start();
 t2.start();

 // 如果没有这俩 join, 肯定不⾏的. 线程还没⾃增完, 就开始打印了. 很可能打印出来的count 就是个 0
 t1.join();
 t2.join();

 // 预期结果应该是 10w
 System.out.println("count: " + count);
}

这里用到的机制synchronized"锁"下一篇为大家讲~~~

相关推荐
像素猎人2 小时前
差分数组【自用笔记】【c++】
c++·笔记·算法
book123_0_992 小时前
spring 跨域CORS Filter
java·后端·spring
空空潍2 小时前
Spring AI 实战教程(一)入门示例
java·后端·spring·ai
星辰_mya2 小时前
自定义注解 + AOP:打造企业级通用组件(日志、限流、幂等)
java·开发语言·spring·面试·架构师
_日拱一卒2 小时前
LeetCode:无重复字符的最长字串
算法·leetcode·职场和发展
polaris06302 小时前
springboot接入deepseek深度求索 java
java·spring boot·后端
hetao17338372 小时前
2026-03-26 ZYZ28-CSP-XiaoMao Round 2 hetao1733837 的 record
c++·算法
dazzle2 小时前
机器学习算法原理与实践-入门(九):基于TensorFlow框架的线性回归
算法·机器学习·tensorflow
真实的菜2 小时前
Spring Boot 升级全攻略:从 2.2 到 2.7 再到 3.x
java·spring boot·后端