当前位置: 首页 > news >正文

多线程(三)-线程安全原因与解决

文章目录

  • 一. 线程安全问题的原因
    • (一) 根本原因
    • (二) 原子性问题
      • >错误实例
      • >解决方案 synchronized关键字
    • (三) 内存可见性问题
      • >错误示例
      • >解决方案 volatile关键字
    • (四) 指令重排序问题
      • >错误实例
      • >解决方案 volatile关键字
    • (五) 多个线程修改同一变量
    • (六) 锁竞争引起的阻塞
      • >错误示例
      • >解决方案

一. 线程安全问题的原因

(一) 根本原因

多个线程之间的调度是随机的,并发执行的, 不公平的, 抢占式执行, 这就导致一个线程的任务可能只执行一半一半, 保存好上下文信息到内存后会执行另一个线程任务, 从而不可避免的产生一些冲突

(二) 原子性问题

多个线程修改同一个变量时, 因为随机调度的问题, 会导致前一个线程修改变量的时候, 还没来得及写回内存, 第二个线程读取还没修改的值, 也进行修改, 最后导致写入内存的值与想要的最终值不匹配

在这里插入图片描述

>错误实例

package test;/*** Created with IntelliJ IDEA.* Description:* User: ran* Date: 2025-07-31* Time: 11:58*/
public class test4 {// **error**private  static int count;public static void main(String[] args) throws InterruptedException {Thread thread1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {count++;}});Thread thread2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {count++;}});thread1.start();thread2.start();thread1.join();thread2.join();System.out.println("count= " + count);}}

在这里插入图片描述

>解决方案 synchronized关键字

通过加锁来使count++成为原子性操作,确保在读取,修改,写入内存的时候不会被其他线程插入

package test;/*** Created with IntelliJ IDEA.* Description:* User: ran* Date: 2025-07-31* Time: 11:58*/
public class test4 {private  static int count;public static void main(String[] args) throws InterruptedException {Object block = new Object();Thread thread1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {synchronized (block) {count++;}}});Thread thread2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {synchronized (block) {count++;}}});thread1.start();thread2.start();thread1.join();thread2.join();System.out.println("count= " + count);}}

在这里插入图片描述

(三) 内存可见性问题

①: 这个与编译器优化机制有关, 当我们写好代码交给编译器编译时, JVM 与 javac会自动优化我们的代码, 在逻辑不变的情况下使程序运行效率提高, 这也导致了一些问题
②: 当我们设置一个变量count作为线程终止条件时, JVM运行过程中会多次获取该变量count的值, 当发现该值长时间没有改变时, 就会将内存中的count值加载到寄存器中, 从而提高运行效率, 于是这时候我们修改count的值写到内存中后, 不会影响寄存器中的 count 的值, 因为优化后寄存器不再从内存中读取该值, 这就导致程序不会正常终止

在这里插入图片描述

>错误示例

package test;import java.util.Scanner;/*** Created with IntelliJ IDEA.* Description:* User: ran* Date: 2025-07-31* Time: 12:31*/
public class test5 {private static int count = 0;public static void main(String[] args) {Thread thread1 = new Thread(() -> {while (count == 0) {}System.out.println("线程1终止!!");});Thread thread2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);System.out.println("输入 1 来终止程序");count = scanner.nextInt();});thread1.start();thread2.start();}
}

在这里插入图片描述

>解决方案 volatile关键字

中文翻译是易变的, volatile 修饰后的变量, 会被标记, 相当于跟JVM摊牌表明了, 这个变量我volatile保了, 他是一定会改变的, 给他一次机会, 请JVM高抬贵手放弃优化该变量吧!, 可以确保被修饰变量的内存可见性

package test;import java.util.Scanner;/*** Created with IntelliJ IDEA.* Description:* User: ran* Date: 2025-07-31* Time: 12:31*/
public class test5 {private volatile static int count = 0;public static void main(String[] args) {Thread thread1 = new Thread(() -> {while (count == 0) {}System.out.println("线程1终止!!");});Thread thread2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);System.out.println("输入 1 来终止程序");count = scanner.nextInt();});thread1.start();thread2.start();}
}

在这里插入图片描述

(四) 指令重排序问题

  1. 也是一种编译器优化机制. 代码交给编译器后, 编译器有时会调整代码的指令执行顺序, 生活场景就是, 超市买醋或者盐:
    ① 一种方式是先买醋或者盐, 结账后再返回买另一个
    ② 另一种方式是: 直接一趟将盐和醋都找到同时结账
    这两种最后结果一样, 但是排序后的效率大大提升
  2. 但在多线程中, 有时候优化也会发生错误,
    例如: 重装系统时, 我们第一步是将Windows 的 iso 镜像下载好后重装系统, 第二步系统装好后去对应官网找对应电脑型号的驱动, 重装系统与打驱动这两步都不能少, 第三步下载需要的产品来使用
    而重排序后, 可能就导致第一步重装系统完成后, 直接就开始第三步去下载想要的产品来使用, 这时候就会触发一系列问题, 没声音, 耗电快, 触摸板无法使用, 连不上无线WIFI 等等…

在这里插入图片描述

>错误实例

单例模式的懒汉模式中, 当你又在 synchronized 的外层加了个if语句时, 会判断当前单例类不为null, 不进入竞争锁的过程, 也就不会产生锁的阻塞. 提高效率, 但是当两个线程同时调用时, 就会发生一个线程修改完成后, 写入内存, 但是另一个线程已经提前拿到null的 instance, 就会使用空的单例类进行一系列非法操作

package demo_thread;/*** Created with IntelliJ IDEA.* Description:* User: 32309* Date: 2025-07-19* Time: 20:18*/
public class demo25_SingleLazy {private  static demo25_SingleLazy instance;// 加volatile 消除指令重排序及内存可见性问题private static Object locker = new Object();public static demo25_SingleLazy getInstance() {// 外层再加一层判断条件,是让当前对象不为空的时候不加锁,提高执行效率if (instance == null) {// 加锁,确保判断条件的及赋值语句的组合的原子性synchronized (locker) {if (instance == null) {return new demo25_SingleLazy();}}}return instance;}}

>解决方案 volatile关键字

volatile关键字不止可以解决内存可见问题, 同样可以阻止重排序

package demo_thread;/*** Created with IntelliJ IDEA.* Description:* User: 32309* Date: 2025-07-19* Time: 20:18*/
public class demo25_SingleLazy {private volatile static demo25_SingleLazy instance;// 加volatile 消除指令重排序及内存可见性问题private static Object locker = new Object();public static demo25_SingleLazy getInstance() {// 外层再加一层判断条件,是让当前对象不为空的时候不加锁,提高执行效率if (instance == null) {// 加锁,确保判断条件的及赋值语句的组合的原子性synchronized (locker) {if (instance == null) {return new demo25_SingleLazy();}}}return instance;}}

(五) 多个线程修改同一变量

首先, 我们要知道线程对变量的所有操作:

  1. 当多个线程修改同一变量时, 会出现问题
  2. 一个线程修改一个/多个变量, 没问题
  3. 多个线程只读取变量的值, 没问题
  4. 多个线程修改不同变量, 没问题

多个线程修改同一变量产生问题的原因主要在于 修改操作不是原子的, 如果使修改操作是原子的, 或者修改代码让一个线程只修改一个变量, 那么问题也就迎刃而解了, 上面原子性问题已经解决过, 这里不多做赘述

(六) 锁竞争引起的阻塞

上篇博客我们讲到了线程的一个属性: BLOCKED 阻塞, 锁竞争产生的阻塞往往在我们的多线程任务重是一个令人头疼的问题, 而我们加锁又确实是为了其阻塞效果带来的原子性, 这里我们就讨论一种严重的阻塞情况, 死锁:

  1. 线程加锁后忘记释放锁会死锁,
  2. 一个线程加锁后因为一些原因在释放锁前异常终止会导致锁无法释放而死锁
  3. N个线程M把锁, 每个线程同时竞争两个锁时, 会死锁

1.针对以上前两个问题, Java标准库提供了一个很厉害的关键字 synchronized, 这把锁的适应能力非常强, 适用于乐观/悲观场景, 自旋/挂起等待…众多场景, 可以说能力非常强大, 同时因为他是一个关键字, 相当于出了 “}” 花括号范围就自动解锁
2. 所以我们只需解决第三个场景: N个线程M把锁

>错误示例

在这里插入图片描述

package test;/*** Created with IntelliJ IDEA.* Description:* User: ran* Date: 2025-07-31* Time: 10:54*/
public class test3 {public static void main(String[] args) {Object lock1 = new Object();Object lock2 = new Object();Object lock3 = new Object();Thread thread1 = new Thread(() -> {synchronized (lock1) {System.out.println("获取第一把锁: lock1");synchronized (lock2) {System.out.println("尝试获取第二把锁: lock2");}}});Thread thread2 = new Thread(() -> {synchronized (lock2) {System.out.println("获取第一把锁: lock2");synchronized (lock3) {System.out.println("尝试获取第二把锁: lock3");}}});Thread thread3 = new Thread(() -> {synchronized (lock3) {System.out.println("获取第一把锁: lock3");synchronized (lock1) {System.out.println("尝试获取第二把锁: lock1");}}});thread1.start();thread2.start();thread3.start();}
}

>解决方案

这道题是典型的哲学家就餐问题, 三个哲学家三根筷子, 但吃饭需要两根筷子且只能拿最近的两双筷子, 他们如果同时拿尝试获取两根筷子, 这时候胡不想让就会阻塞, 于是我们可以换种思路, 让第一个人先拿第一根筷子, 让第二个人先拿第二根筷子, 以此类推, 但是最后一个人要先尝试拿第一根筷子, 拿不到先等待, 这时候, 倒数第二个人可以趁机获取到最后一根与前一根筷子, 从而完成就餐, 整个问题迎刃而解!

在这里插入图片描述

package test;/*** Created with IntelliJ IDEA.* Description:* User: ran* Date: 2025-07-31* Time: 10:54*/
public class test3 {public static void main(String[] args) {Object lock1 = new Object();Object lock2 = new Object();Object lock3 = new Object();Thread thread1 = new Thread(() -> {synchronized (lock1) {System.out.println("获取第一把锁: lock1");synchronized (lock2) {System.out.println("尝试获取第二把锁: lock2");}}});Thread thread2 = new Thread(() -> {synchronized (lock2) {System.out.println("获取第一把锁: lock2");synchronized (lock3) {System.out.println("尝试获取第二把锁: lock3");}}});Thread thread3 = new Thread(() -> {synchronized (lock1) {System.out.println("获取第一把锁: lock3");synchronized (lock3) {System.out.println("尝试获取第二把锁: lock1");}}});thread1.start();thread2.start();thread3.start();}
}

在这里插入图片描述

http://www.dtcms.com/a/312508.html

相关文章:

  • Day26-二叉树的最小深度
  • 【软考中级网络工程师】知识点之 RIP 协议
  • C++ 之 【模拟实现 优先级队列】
  • SQL 地理空间原理与实现
  • slice() 和 splice()
  • 信创及一次ORACLE到OB的信创迁移
  • 自由学习记录(76)
  • Python 的标准库 bisect 模块
  • 源码交易平台排行榜
  • 机器学习 决策树基本介绍
  • Mysql的MVCC是什么
  • HCIE-Datacom题库_07_设备【道题】
  • 《深入解析 Python 的 `*args` 和 `**kwargs`:从基础使用到高级应用》
  • 【数据结构】哈希表实现
  • 网关和BFF是如何演化的
  • uniapp 跨端开发
  • 基于Springboot+UniApp+Ai实现模拟面试小工具八:管理端基础功能实现
  • (论文速读)探索多模式大型语言模型的视觉缺陷
  • DeepSeek 论文夺冠,智谱开源 GLM-4.5,OpenAI 学习模式上线!| AI Weekly 7.28-8.3
  • 基于机器学习的Web应用漏洞分析与预测系统,使用django框架,三种机器学习模型
  • 深入探讨AI在测试领域的三大核心应用:自动化测试框架、智能缺陷检测和A/B测试优化,并通过代码示例、流程图和图表详细解析其实现原理和应用场景。
  • 关于Web前端安全防御之内容安全策略(CSP)
  • 知识蒸馏 - 基于KL散度的知识蒸馏 HelloWorld 示例 采用PyTorch 内置函数F.kl_div的实现方式
  • 【Linux系统】进程间通信:匿名管道
  • AI 时代的 IT 从业者:共生而非替代
  • 人声伴奏分离API:音乐智能处理的强大工具
  • Spring AI 项目实战(二十二):Spring Boot + AI +DeepSeek实现智能合同数据问答助手​(附完整源码)
  • 小白学OpenCV系列2-理解图像
  • MySQL--高可用MHA集群详解及演练
  • SelectDB数据库,新一代实时数据仓库的全面解析与应用