[Java EE] 多线程 -- 初阶(3)

6.3 死锁

死锁的核心 : 多个线程互相持有对方所需要的锁且都不释放

① 情况一 : 一个线程获取同一把锁 , 并加锁多次

这种情况在 java 中不会出现死锁

② 情况二 : 两个线程 , 两把锁 , 每个线程获取到一把锁后 , 尝试获取另一把锁 , 会产生死锁

java 复制代码
public class demo20 {
    public static void main(String[] args) throws InterruptedException {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Thread t1 = new Thread(()->{
            synchronized (locker1){
                System.out.println("开始执行t1线程");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException();
                }
                synchronized (locker2){
                    System.out.println("t1线程结束");
                }
            }
        });
        Thread t2 = new Thread(()->{
            synchronized (locker2){
                System.out.println("开始执行t2线程");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException();

                }
                synchronized (locker1){
                    System.out.println("t2线程结束");
                }
            }
        });
        System.out.println("main线程开始");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("main线程结束");
    }
}

但为了两个线程都能获取到锁 , 必须在每个线程获取到第一个锁后加入Thread.sleep(1000) ; 两个线程相互竞争 , 互不相让 ; (家钥匙锁车里了 , 车钥匙锁家里了)

只要线程中出现交叉等待锁的情况 , 仍然可能死锁

6.4 如何解决或避免死锁

① 构成死锁的必要条件

  1. 锁是互斥的 , 一个线程拿到锁之后 , 另一个线程在尝试获取锁 , 必然会阻塞等待
  2. 锁是不可抢占的 , 线程1 拿到锁 , 线程2 也尝试获取这个锁 , 线程2 必会陷入阻塞等待
  3. 请求和保持 , 一个线程拿到锁1 之后 , 不释放锁1 的前提下获取锁 2 ; 也就是 使用锁的时候尽量避免嵌套
  4. 循环等待 , 多个线程 , 多把锁之间的等待过程构成了循环 ; A 等待 B , B 也等待 A ;

② 解决死锁

在 Java 中 synchronized 是遵守前两个条件的 ; 所以只能从后两点解决

1) 请求和保持
java 复制代码
public class demo21 {

    public static void main (String[]args) throws InterruptedException {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (locker1) {
                System.out.println("开始执行t1线程");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException();
                }
            }

            synchronized (locker2) {
                System.out.println("t1线程结束");
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized (locker2) {
                System.out.println("开始执行t2线程");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException();

                }
            }

            synchronized (locker1) {
                System.out.println("t2线程结束");
            }
        });
        System.out.println("main线程开始");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("main线程结束");
    }
}

解决核心 : 避免锁嵌套

2) 循环等待
java 复制代码
public class demo21 {
    public static void main (String[]args) throws InterruptedException {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (locker1) {
                System.out.println("开始执行t1线程");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException();
                }
                synchronized (locker2) {
                    System.out.println("t1线程结束");
                }
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized (locker1) {
                System.out.println("开始执行t2线程");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException();

                }
                synchronized (locker2) {
                    System.out.println("t2线程结束");
                }
            }
        });
        System.out.println("main线程开始");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("main线程结束");
    }
}

解决核心 : 约定每个线程加锁的顺序 , 例如 : 按序号从小到大的顺序进行加锁

7.再谈内存可见性问题

7.1 可见性

一个线程对共享变量值的修改,能够及时的被其他线程看到

7.2 内存可见性

一个线程对共享变量的修改 , 其他线程不能及时看到

这是由于 JVM 的内存模型(JMM)中 , 线程会将共享变量从主内存拷贝到自己工作内存中进行操作 , 若缺乏同步机制 , 不同线程的工作内存数据可能不一致

7.3 产生原因

① CPU 缓存机制

现代 CPU 有多级缓存 , 线程操作变量时 先再缓存中执行 , 未及时同步到主内存 , 其他线程从主内存读取的还是旧值

② 编译器优化

编译器可能对代码进行重排序或缓存变量(如将变量缓存到寄存器) , 导致变量修改对其他线程不可见

实例说明 :

java 复制代码
import java.util.Scanner;

public class demo22 {
    private static int flag = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            while(flag == 0){

            }
            System.out.println("t1线程结束");
        });
        Thread t2 = new Thread(()->{
            //针对flag进行修改
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入flag的值");
            flag = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

上述代码中 , 线程2 修改 flag 后 , 线程1 可能因内存可见性问题一直卡再 while 循环中 , 无法感知 flag 的变化

解决方法

① 对 while 循环微调 , 让编译器不做优化
java 复制代码
import java.util.Scanner;

public class demo22 {
    private static int flag = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            while(flag == 0) {
                try {
                    Thread.sleep(1);
                }catch (InterruptedException e){
                    throw new RuntimeException();
                }
            }
            System.out.println("t1线程结束");
        });
        Thread t2 = new Thread(()->{
            //针对flag进行修改
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入flag的值");
            flag = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

本质上 : 增加了每次 while 循环的时间 , 此时编译器就不会将 读内存操作优化为度寄存器操作了 , 因为优化的时间无足轻重

② volatile 关键字

作用 : 会禁止编译器和 CPU 的重排序优化 , 且写操作会立即同步到主内存 , 读操作会从主内存中读取 , 从而保证可见性

java 复制代码
import java.util.Scanner;

public class demo22 {
    private volatile static int flag = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            while(flag == 0) {

            }
            System.out.println("t1线程结束");
        });
        Thread t2 = new Thread(()->{
            //针对flag进行修改
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入flag的值");
            flag = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

注意 : volatile 不保证原子性 , 与 sunchronized 着本质的区别

8. wait() 和 notify()

在 Java 中 wait() 和 notify()是 Object 类的方法 , 用于线程间的协作 , 通常配合 synchronized 关键字使用 , 实现线程的等待与唤醒机制

8.1 核心作用 :

wait() / wait(long timeout) :

作用 : 让当前线程释放持有的锁 , 并进入阻塞等待 , 直到其他线程调用同一对象的 notify()或 notifyAll()唤醒

wait 结束等待的条件 : ① 其他线程调用相同锁对象的 notify()方法 ; ②wait(timeout) 的等待时间超时 ; ③ 其他线程调用该线程的 interrupted 方法 , 导致 wait 抛出 InterruptedException 异常

执行结果 : 从控制台看出 , 程序执行到 wait()后进入阻塞 , 这符合 wait()方法的特性 -- 若无其他线程唤醒 , 当前线程会一直等待

notify() :

唤醒在次对象锁上等待的任意一个线程(如果是多个线程对应同一个锁对象 , 那么具体唤醒哪个是随机的) , 使其从等待状态进入就绪状态 , 重新竞争锁

使用时 务必要确保先 wait()再 notify()才会起作用 ;

notifyAll() :

唤醒在此对象锁上等待的所有线程 , 让它们重新竞争锁(但不要过于依赖)

示例 1 :

java 复制代码
import java.util.Scanner;

public class demo24 {
    public static void main(String[] args) {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Thread t1 = new Thread(()->{
            try{
                System.out.println("wait之前");
                synchronized (locker1){
                    locker1.wait();//需要借助锁对象操作
                }
                System.out.println("wait之后");
            }catch (InterruptedException e){
                throw new RuntimeException();
            }
        });
        Thread t2 = new Thread(()->{
            Scanner scanner = new Scanner(System.in);
            System.out.println("输入任意内容唤醒 t1");
            scanner.next();
            synchronized (locker1){
                locker1.notify();//同样需要锁锁对象操作
            }
        });
        t1.start();
        t2.start();
    }
}

使用条件 :

  1. 必须在 synchronized 代码块中调用

因为 wait() 和 notify() 需要操作对象的锁 , 调用前必须确保当前线程已获取该对象的锁(否则抛出异常)

  1. 操作的必须是同一把锁

线程 A 在锁对象lock上调用wait(),只有线程 B 在同一lock上调用notify(),才能唤醒线程 A

示例 2 :

java 复制代码
import java.util.Scanner;

public class demo25 {
    public static void main(String[] args) {
        Object locker = new Object();
        Thread t1 = new Thread(()->{
            try{
                System.out.println("t1 wait之前");
                synchronized (locker){
                    locker.wait();
                }
                System.out.println("t1 wait之后");
            }catch (InterruptedException e){
                throw new RuntimeException();
            }
        });
        Thread t2 = new Thread(()->{
            try{
                System.out.println("t2 wait之前");
                synchronized (locker){
                    locker.wait();
                }
                System.out.println("t2 wait之后");

            }catch (InterruptedException e){
                throw new RuntimeException();
            }
        });
        Thread t3 = new Thread(()->{
            Scanner scanner = new Scanner(System.in);
            System.out.println("输入任意内容唤醒所有线程");
            scanner.next();
            synchronized (locker){
                locker.notifyAll();
            }
        });
        //        Thread t3 = new Thread(()->{
        //            Scanner scanner = new Scanner(System.in);
        //            System.out.println("输入任意内容,唤醒线程");
        //            scanner.next();
        //            synchronized (locker){
        //                locker.notify();
        //            }
        //            System.out.println("输入任意内容,唤醒另一个线程");
        //            scanner.next();
        //            synchronized (locker){
        //                locker.notify();
        //            }
        //
        //        });
        t1.start();
        t2.start();
        t3.start();
    }

8.3 与sleep()的区别

|--------|--------------------|-----------------|
| 特性 | wait() | sleep(long) |
| 锁释放 | 释放持有的锁 | 不释放锁 |
| 所属类 | Object 类 | Thread 类 |
| 使用场景 | 线程间协作(等待 / 唤醒) | 单纯延迟执行 |
| 唤醒方式 | 需其他线程notify() 唤醒 | 时间到后自动唤醒 |

相关推荐
2301_795167201 小时前
玩转Rust高级应用 如何理解 Rust 实现免疫数据竞争的关键是Send 和 Sync 这两个 trait
开发语言·算法·rust
云和数据.ChenGuang2 小时前
Python 3.14 与 PyCharm 2025.2.1 的调试器(PyDev)存在兼容性问题
开发语言·python·pycharm
Felix_XXXXL2 小时前
MySQL----case的用法
java·后端
Mr.Jessy2 小时前
Web APIs 学习第六天:BOM、location对象与本地存储
开发语言·前端·javascript·学习·web api·bom
LIZhang20162 小时前
基于ffmpeg8.0录制mp4文件
开发语言·c++
咕白m6252 小时前
基于Java 实现 PPT 到 PDF 的高效转换
java
_OP_CHEN3 小时前
C++进阶:(九)深度剖析unordered_map 与 unordered_set容器
开发语言·c++·stl容器·哈希表·哈希桶·unordered_map·unordered_set
七夜zippoe3 小时前
Java并发编程基石:深入理解JMM(Java内存模型)与Happens-Before规则
java·开发语言·spring·jmm·happens-before
Mark Studio3 小时前
QT linux 静态编译问题记录
开发语言·qt