多线程(三)-线程安全原因与解决
文章目录
- 一. 线程安全问题的原因
- (一) 根本原因
- (二) 原子性问题
- >错误实例
- >解决方案 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();}
}
(四) 指令重排序问题
- 也是一种编译器优化机制. 代码交给编译器后, 编译器有时会调整代码的指令执行顺序, 生活场景就是, 超市买醋或者盐:
① 一种方式是先买醋或者盐, 结账后再返回买另一个
② 另一种方式是: 直接一趟将盐和醋都找到同时结账
这两种最后结果一样, 但是排序后的效率大大提升- 但在多线程中, 有时候优化也会发生错误,
例如: 重装系统时, 我们第一步是将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;}}
(五) 多个线程修改同一变量
首先, 我们要知道线程对变量的所有操作:
- 当多个线程修改同一变量时, 会出现问题
- 一个线程修改一个/多个变量, 没问题
- 多个线程只读取变量的值, 没问题
- 多个线程修改不同变量, 没问题
多个线程修改同一变量产生问题的原因主要在于 修改操作不是原子的, 如果使修改操作是原子的, 或者修改代码让一个线程只修改一个变量, 那么问题也就迎刃而解了, 上面原子性问题已经解决过, 这里不多做赘述
(六) 锁竞争引起的阻塞
上篇博客我们讲到了线程的一个属性: BLOCKED 阻塞, 锁竞争产生的阻塞往往在我们的多线程任务重是一个令人头疼的问题, 而我们加锁又确实是为了其阻塞效果带来的原子性, 这里我们就讨论一种严重的阻塞情况, 死锁:
- 线程加锁后忘记释放锁会死锁,
- 一个线程加锁后因为一些原因在释放锁前异常终止会导致锁无法释放而死锁
- 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();}
}