# 20260811 课堂笔记 — volatile、Lock 锁与线程池 - **日期**:2026-08-11 - **项目**:`c260811` - **包路径**:`course` / `homework0810` - **作者**:WanJL --- ## 目录 1. [volatile 关键字概述:解决可见性和有序性,不能保证原子性](#1-volatile-关键字概述解决可见性和有序性不能保证原子性) 2. [可见性问题与 JVM 内存模型:工作内存(CPU 缓存)vs 主内存](#2-可见性问题与-jvm-内存模型工作内存cpu-缓存vs-主内存) 3. [可见性问题经典演示:程序永不退出(VolatileDemo)](#3-可见性问题经典演示程序永不退出volatiledemo) 4. [volatile 三大语义:可见性 / 禁止重排序 / 不保证原子性](#4-volatile-三大语义可见性--禁止重排序--不保证原子性) 5. [线程安全三大特性解决方案回顾](#5-线程安全三大特性解决方案回顾) 6. [计数器问题升级:volatile 不能保证原子性,synchronized 可以(Counter)](#6-计数器问题升级volatile-不能保证原子性synchronized-可以counter) 7. [Lock 锁:ReentrantLock 可重入锁(Demo02 / LockDemo)](#7-lock-锁reentrantlock-可重入锁demo02--lockdemo) 8. [死锁:产生条件与避免方法(Demo03)](#8-死锁产生条件与避免方法demo03) 9. [线程池:ThreadPoolExecutor(Demo04 / ThreadPoolDemo)](#9-线程池threadpoolexecutordemo04--threadpooldemo) 10. [课后作业回顾:synchronized 修复卖票问题(homework0810 Demo_03)](#10-课后作业回顾synchronized-修复卖票问题homework0810-demo_03) 11. [知识点全景总结](#11-知识点全景总结) 12. [随堂练习要点](#12-随堂练习要点) 13. [拓展阅读](#13-拓展阅读) --- ## 1. volatile 关键字概述:解决可见性和有序性,不能保证原子性 ### 概念 —— volatile 是解决可见性 / 有序性的"轻量级"方案 **volatile** 是 Java 的关键字,用来**解决线程安全的可见性问题和有序性问题**,但**不能保证原子性**。它正是上节课(20260810)线程安全三大特性中"可见性、有序性"两个问题的解决方案之一。 ```java // 来源:course/Demo01.java(类注释) /* volatile是用来解决可见性和有序性问题的,不能保证原子性。 可见性问题:一个线程对共享数据的修改,对其他线程不可见,导致其他线程无法做出相应的操作和响应。 volatile是可以解决程序永不退出的问题。 不加volatile可能导致程序永不退出。 可见性问题:程序永不退出: 代码看VolatileDemo.java */ ``` | 解决的能力 | 说明 | |------------|------| | **解决可见性** | 一个线程对共享数据的修改,对其他线程**立即可见**,其他线程能做出相应操作和响应 | | **解决有序性** | 禁止编译器和 CPU 对 volatile 读写及其前后指令进行**重排序** | | **不解决原子性** | `volatile count++` 依旧会丢失更新,做不到让"读-改-写"3 步变成 1 步 | > **要点**: > - **volatile 是三大特性中可见性、有序性的解决方案**(回顾上节课):可见性 → volatile / synchronized / Lock;有序性 → volatile / synchronized > - **原子性只能靠锁或原子类**:synchronized、Lock、Atomic 类——volatile 无法保证 count++ 的原子性 > - **volatile 的定位**:比 synchronized 更"轻量"——只解决可见性与有序性,不涉及锁机制 --- ## 2. 可见性问题与 JVM 内存模型:工作内存(CPU 缓存)vs 主内存 ### 概念 —— 为什么一个线程的修改另一个线程看不见 **可见性问题**的根源在 JVM 内存模型:**每个线程都有自己的独立工作内存(其实就是 CPU 缓存)**,线程读写变量时**优先从工作内存**操作,而不是每次直接读写主内存的真实值。 ```java // 来源:course/Demo01.java(类注释) /* 在JVM内存模型中,每个线程都有自己的独立的工作内存(其实就是CPU缓存),线程读取变量的时候会优先 从工作内存读,而不是再次读取真实的变量值。写变量的时候也是会先写入到工作内存,再刷新到主内存。 而且这个刷新也不是立即发生的,它是可能会有延迟的。 所以就可能出现:main线程把running属性改为了false并写入到了主内存。 但是worker线程一直读的都是自己的CPU缓存(工作内存)的旧值(true),导致while(running)永不退出,这就是可见性问题。 */ ``` ### JVM 内存模型示意图 ``` ┌──────────────────────────────────────────────────────────┐ │ CPU 核心 1 —— worker 线程 │ │ L1/L2 缓存 running = true(旧值缓存) │ ├──────────────────────────────────────────────────────────┤ │ CPU 核心 2 —— main 线程 │ │ L1/L2 缓存 running = false │ ├──────────────────────────────────────────────────────────┤ │ 主内存 running = false │ └──────────────────────────────────────────────────────────┘ worker 的缓存 -.需要及时刷新.-> 主内存 main 的缓存 -.写操作会立即刷回.-> 主内存 加 volatile 后:写 volatile 变量立即刷主存, 读 volatile 变量强制从主存读 = 禁止使用线程本地缓存 ``` > **要点**: > - **工作内存 = CPU 缓存**:每个线程有独立的 L1/L2 缓存,读写变量**优先走工作内存**,不会每次都访问主内存 > - **刷新有延迟**:线程写变量先写工作内存再刷新主内存,**刷新不是立即发生、可能有延迟** > - **可见性问题的产生**:main 线程把 `running` 改为 false 并写入主内存,但 worker 线程**一直读自己 CPU 缓存里的旧值 true** → `while(running)` 永不退出 > - **volatile 的作用**:**写 volatile 变量立即刷主存,读 volatile 变量强制从主存读**——禁止使用线程本地缓存,从而解决可见性 --- ## 3. 可见性问题经典演示:程序永不退出(VolatileDemo) ### 演示目标 不加 volatile 时,主线程修改 `running` 为 false,但 worker 线程一直读旧值 true,导致**程序永不退出**;加了 `volatile` 后,worker 线程能立刻看到修改,正常退出。 ```java // 来源:course/VolatileDemo.java public class VolatileDemo { //如果不加volatile,其他线程可能永远都看不到修改 private volatile boolean running=true; /** * 线程工作的方法 */ public void work(){ System.out.println("work 线程开始......"); while (running){ //空循环,什么都不做.... //System.out.println("work...."); } System.out.println("线程退出......"); } /** * 停止线程工作的方法 */ public void stop(){ running=false; } public static void main(String[] args) throws InterruptedException { VolatileDemo vd=new VolatileDemo(); Thread t=new Thread(vd::work,"worker"); /* 相当于: new Thread(()->{ vd.work(); },"worker") 相当于: new Thread(new Runnable(){ @Override public void run(){ vd.work(); } },"worker") */ t.start(); Thread.sleep(1000); //主线程停止vd对象的工作 vd.stop(); } } ``` ### 运行流程拆解 ``` 1、VolatileDemo vd = new VolatileDemo() 创建对象,running 初始为 true 2、Thread t = new Thread(vd::work,"worker") 创建 worker 线程,任务方法是 vd.work() 3、t.start() 启动 worker 线程 → 打印"开始...",进入 while(running) 空循环 4、main 线程 Thread.sleep(1000) 让 worker 先跑起来(此时 running 还是 true) 5、vd.stop() 主线程把 running 改为 false ├─ 不加 volatile:worker 读的是自己 CPU 缓存里的旧值 true → while 永不退出("线程退出..."打不出来) └─ 加了 volatile:worker 强制从主存读 false → while 退出,打印"线程退出......" ``` > **要点**: > - **volatile 解决"程序永不退出"**:这正是可见性问题的经典现场——worker 线程读不到主线程的修改 > - **while(running) 空循环**:循环体为空(sleep 都没有),worker 线程疯狂读 `running`——**读的始终是工作内存里的旧值** > - **`vd::work` 方法引用**:等价于 `() -> vd.work()`,也等价于匿名 Runnable(回顾 08-04 方法引用 + 08-08 Runnable 方式) > - **演示要点**:`Thread.sleep(1000)` 让 worker 先跑起来,再 `stop()` 修改——对比加/不加 volatile 时是否打印"线程退出......" --- ## 4. volatile 三大语义:可见性 / 禁止重排序 / 不保证原子性 ### 概念 —— volatile 承诺了什么、不承诺什么 ```java // 来源:course/Demo01.java(类注释) /* volatile语义: 1、可见性:写volatile修饰的变量,会立刻刷新到主内存,读volatile变量会强制从主内存读,禁止使用CPU缓存。 2、禁止重排序(有序性):一般情况下编译器和CPU会对代码指令进行重排序以方便运行,但是如果使用了volatile,编译器和CPU就不会把volatile的读写操作以及它前后的指令进行指令重排。 3、不保证原子性:volatile count++,它依旧会丢失更新,做不到让3步变为1步。必须要synchronized/lock/原子类 */ ``` | # | volatile 语义 | 说明 | |---|--------------|------| | 1 | **可见性** | 写 volatile 变量**立刻刷新到主内存**;读 volatile 变量**强制从主内存读**,禁止使用 CPU 缓存 | | 2 | **禁止重排序(有序性)** | 编译器和 CPU **不会**把 volatile 的读写操作以及它**前后的指令**进行**指令重排** | | 3 | **不保证原子性** | `volatile count++` **依旧会丢失更新**,做不到让 3 步变 1 步——必须用 synchronized / Lock / 原子类 | > **要点**: > - **语义 1 可见性**:解决第 2、3 节"程序永不退出"的问题——写立即刷主存、读强制从主存读 > - **语义 2 有序性**:默认情况下编译器和 CPU 会**重排指令以方便运行**;用了 volatile 后,volatile 读写及其前后指令**不被重排**,代码按书写顺序执行 > - **语义 3 不保证原子性**:volatile **不能替代 synchronized / 原子类**——`count++` 的"读-改-写"3 步依旧可能被打断(第 6 节 Counter 验证) --- ## 5. 线程安全三大特性解决方案回顾 ### 概念 —— 把三大特性的解决方案对齐 volatile volatile 正是三大特性中"可见性、有序性"的解决方案之一,这节把上节课(20260810)的三大特性与解决方案做一个**整体回顾对齐**: | 特性 | 含义 | 解决的问题 | 解决方案 | |------|------|-----------|----------| | **原子性** | 操作**不可分割**,要么全部执行、要么全部不执行 | 防止"读-改-写"被并发打断 | `synchronized`、`Lock`(锁)、`Atomic` 类 | | **可见性** | 一个线程的修改对另一个线程**立即可见** | 防止线程把变量缓存到 CPU 缓存中,其他线程看不到修改 | **`volatile`**、`synchronized`、`Lock`(锁) | | **有序性** | 代码**按书写顺序执行**,不被**指令重排** | 防止编译器和 CPU 对指令重排 | **`volatile`**、`synchronized` | > **要点**: > - **volatile 的两把刷子**:只解决**可见性**和**有序性**,是这两个特性的解决方案之一 > - **原子性不归 volatile 管**:要保证原子性必须用 synchronized / Lock / 原子类 > - **volatile vs synchronized**:volatile 轻量、不涉及锁、只解决可见性/有序性;synchronized 重量、加锁、同时保证三大特性(原子性 + 可见性 + 有序性) --- ## 6. 计数器问题升级:volatile 不能保证原子性,synchronized 可以(Counter) ### 演示目标 延续上节课的**计数器问题**(1000 线程并发 count++,理论 1000 实际小于 1000)——这次给 `count` 加了 **volatile** 修饰,验证:**volatile 依然不能保证原子性**,count 还是小于 1000;只有加 **synchronized** 的 increment() 才能保证原子性,count 必然是 1000。 ```java // 来源:course/Counter.java public class Counter { //使用volatile修饰也不能保证原子性 private volatile Integer count=0; //只要加了synchronized,就能够保证原子性 public synchronized void increment(){ count++; //看似是1行,但底层是3步 //1、拿到当前count的值 //2、count+1 //3、自增后的值赋值给count } public Integer getCount() { return count; } public static void main(String[] args) throws InterruptedException { Counter counter=new Counter(); int threadCount=1000; Thread[] threads=new Thread[threadCount]; for (int i = 0; i < threadCount; i++) { threads[i]=new Thread(()->{ counter.increment(); //执行相加 }); threads[i].start(); } //等待所有线程结束 for (Thread t:threads){ t.join(); } //理论上应该是1000,但是实际上往往小于1000 System.out.println("最终 count="+counter.getCount()); } } ``` ### 分析要点 | 关注点 | 说明 | |--------|------| | **volatile 修饰 count** | `private volatile Integer count=0` —— volatile 能保证 count 的**可见性**(各线程读到最新值) | | **但原子性仍然缺失** | `count++` 底层是"**读-改-写**"3 步:①拿到当前 count 值 ②count+1 ③赋值给 count——volatile 无法把这 3 步变成 1 步 | | **synchronized increment()** | 同步实例方法(锁 this),同一时刻只有一个线程能执行 count++ → **保证原子性** | | **1000 线程 + join 等待** | 1000 个线程并发执行 increment(),join() 等所有线程结束后打印结果 | > **要点**: > - **volatile 修了可见性,没修原子性**:即使 count 被 volatile 修饰(所有线程能读到最新值),但 **count++ 的"读-改-写"3 步依然可能被打断**——多个线程读旧值 → 计算 → 覆盖,结果依旧小于 1000 > - **synchronized 才是原子性的保证**:`public synchronized void increment()` 是同步实例方法(锁 this),保证同一时刻只有一个线程执行 count++ → **count 必然是 1000** > - **join() 等待所有线程**:`for (Thread t : threads) t.join()` 让 main 等 1000 个线程全部结束后再打印(回顾 08-10 join) > - **对比上节课 Counter**:上节课 Counter 的 count 是普通 `Integer`,这节课给 count 加了 volatile——**结果照样小于 1000**,直观证明"volatile 不能保证原子性" > - **三方案对比**:普通变量(丢更新)< volatile(丢更新,但解决可见性)< synchronized(原子性保证,count=1000) --- ## 7. Lock 锁:ReentrantLock 可重入锁(Demo02 / LockDemo) ### 概念 —— JDK1.5 提供的更精细的锁机制 除了 `synchronized`,Java 从 **JDK 1.5** 开始提供 **`java.util.concurrent.locks.Lock` 接口**以及它的实现类 **`ReentrantLock`(可重入锁)**。相比于 synchronized,Lock 提供了**更精细的锁控制**,但使用上有一条规定动作必须严格遵守: ```java // 来源:course/Demo02.java(类注释) /* 从JDK1.5开始,Java就提供了java.util.concurrent.locks.Lock接口以及它的实现类ReentrantLock(可重入锁)。 相比于synchronized,Lock提供了更精细的锁控制,比如可以进行超时等待(拿不到锁了不会再傻等),可以响应中断(等等锁的过程中可以被interrupt()进行中断) 可以指定公平性(可以按照申请顺序获取锁),而且还支持多个条件变量(可以精准的唤醒指定的线程) 但是使用Lock必须要记住一条铁律: synchronized是自动释放锁的(在方法结束或出现异常的时候)。 但是Lock必须要手动释放锁,而且unlock() 释放锁的方法必须要放到finally块中,否则一旦代码抛异常或提前return,锁就永远不会被释放了,其他线程也会永远阻塞(死锁) */ ``` ### synchronized vs Lock 对比(七维对比,Demo02 本次更新扩展) ```java // 来源:course/Demo02.java(类注释,本次更新新增完整七维对比) /* 语法简洁度: synchronized--更简单,只需要关键字就可以了 ReentrantLock--比较复杂,需要try-finally块 锁释放: synchronized--自动释放(方法结束、出现异常都会自动释放锁) ReentrantLock--必须要手动释放unlock() 是否可中断: synchronized--不可中断不支持中断 ReentrantLock--支持中断,lockInterruptibly()可以设置可中断锁 是否可以超时等待: synchronized--不支持超时等待 ReentrantLock--支持超时等待,tryLock(timeout) 是否支持锁读: synchronized--不支持 ReentrantLock--是属于读写分离,单独上锁 底层实现: synchronized-- 监视器锁(Monitor Lock) ReentrantLock-- AQS (AbstractQueuedSynchronizer)抽象队列同步器 JDK21版本更新: synchronized-- 进行了大幅度的优化 ReentrantLock-- 性能相近 */ ``` | 对比维度 | synchronized | Lock(ReentrantLock) | |----------|--------------|----------------------| | **语法简洁度** | **更简单**,只需要关键字即可 | 比较复杂,需要 **try-finally** 块 | | **锁释放** | **自动释放**(方法结束、出现异常都会自动释放锁) | **必须手动释放** `unlock()` | | **是否可中断** | 不可中断,不支持中断 | **支持中断**,`lockInterruptibly()` 可设置可中断锁 | | **是否可以超时等待** | 不支持超时等待 | **支持超时等待**,`tryLock(timeout)` | | **是否支持锁读** | 不支持 | **支持(读写分离)**,单独上锁 | | **底层实现** | **监视器锁(Monitor Lock)** | **AQS(AbstractQueuedSynchronizer)抽象队列同步器** | | **JDK21 版本更新** | **进行了大幅度的优化** | **性能相近** | | **公平性**(概念部分) | 非公平 | **可指定公平锁**——按申请顺序获取锁 | | **条件变量**(概念部分) | 单条件 | **支持多个条件变量**——可精准唤醒指定线程 | > **要点**: > - **Lock 的优势**:①**超时等待**(拿不到锁不会再傻等)②**响应中断**(等待锁的过程中可以被 interrupt() 中断)③**指定公平性**(按申请顺序获取锁)④**多个条件变量**(精准唤醒指定线程)⑤**支持锁读(读写分离)**——单独上锁 > - **使用 Lock 的铁律**:synchronized 是**自动释放锁**的;Lock **必须手动释放锁**,且 `unlock()` **必须放到 finally 块中**——否则代码抛异常或提前 return 时锁永远不会释放,其他线程将永远阻塞(**死锁**) > - **底层实现差异**:synchronized 基于**监视器锁(Monitor Lock)**;ReentrantLock 基于 **AQS(AbstractQueuedSynchronizer)抽象队列同步器**——这是两者最本质的机制区别 > - **JDK21 新变化**:JDK21 中 synchronized **进行了大幅度优化**,与 ReentrantLock **性能相近**——简单场景用 synchronized 更合适 > - **Lock 的定位**:更精细的锁控制,适合复杂并发场景;synchronized 简单可靠,适合一般场景 ### ReentrantLock 的构造方法与核心方法 ```java // 来源:course/Demo02.java(类注释,本次更新补充 lockInterruptibly) /* public ReentrantLock() 创建一个可重入锁对象,默认是非公平锁,释放后所有线程竞争锁资源。 public ReentrantLock(boolean fair) 创建一个可重入锁对象,如果fair是true,则创建公平锁,按照申请顺序获取锁,性能略低 public void lock() 加锁 public void unlock() 释放锁 public boolean tryLock() 尝试获取锁,获取不到就算了 public boolean tryLock(long timeout, TimeUnit unit) 尝试获取锁 并设置等待时间和时间单位 public void lockInterruptibly() 设置可中断的锁 */ ``` | 方法 | 作用 | |------|------| | `new ReentrantLock()` | 创建可重入锁对象,**默认非公平锁**(释放后所有线程竞争锁资源) | | `new ReentrantLock(true)` | 创建**公平锁**,按申请顺序获取锁(性能略低) | | `void lock()` | **加锁** | | `void unlock()` | **释放锁**(必须放 finally) | | `boolean tryLock()` | **尝试获取锁**,获取不到就算了(立即返回 false) | | `boolean tryLock(long timeout, TimeUnit unit)` | 尝试获取锁,并设置**等待时间和时间单位**(限时等待) | | `void lockInterruptibly()` | **设置可中断的锁**——获取锁时等待过程中**可被 interrupt() 中断**(LockDemo 使用) | ### LockDemo 四种用法 —— 基本用法:lock() + unlock() ```java // 来源:course/LockDemo.java(基本用法) public class LockDemo { private Integer count=0; //创建锁对象---默认是非公平锁 ReentrantLock lock=new ReentrantLock(); //new ReentrantLock(true); 公平锁,按照申请顺序获取锁,性能略低 //---------基本用法:lock()+unlock() ,前提是必须要finally public void increment(){ //加锁-- lock() lock.lock(); try{ count++; }finally {// 释放锁,必须要释放,否则其他线程永远拿不到锁 lock.unlock(); } } ``` > **要点**: > - **标准模板**:`lock.lock()` 加锁 → `try { 共享数据操作 } finally { lock.unlock() }`——unlock 必须放 finally,否则异常时锁不释放 > - **非公平锁默认**:`new ReentrantLock()` 默认非公平锁(释放后所有线程竞争);`new ReentrantLock(true)` 创建公平锁(性能略低) ### LockDemo 四种用法 —— 进阶 1:tryLock() 尝试获取锁(避免死锁的手段) ```java // 来源:course/LockDemo.java(tryLock 用法) //--------进阶用法:tryLock()尝试获取锁,如果实在拿不到就算了,继续干别的。避免死锁的手段。 public boolean tryIncrement(){ if (lock.tryLock()){ //立刻尝试获取锁,获取到了就返回true,否则返回false try { count++; return true; }finally { lock.unlock(); } } return false; //没抢到锁就返回false } ``` > **要点**: > - **tryLock() 立刻尝试**:获取到锁返回 true,获取不到**立即返回 false**(不等待) > - **拿不到就算了**:没抢到锁就 `return false`,线程继续干别的——这是**避免死锁的手段**(不会一直阻塞等锁) ### LockDemo 四种用法 —— 进阶 2:tryLock(timeout) 限时等待 ```java // 来源:course/LockDemo.java(tryLock(timeout) 用法) //--------进阶用法:tryLock(timeout)限时等待 public boolean timedIncrement(){ try { if (lock.tryLock(1, TimeUnit.SECONDS)){ //最多等待1秒 try { count++; return true; }finally { lock.unlock(); //释放锁 } } } catch (InterruptedException e) { throw new RuntimeException(e); } return false; } ``` > **要点**: > - **tryLock(timeout) 限时等待**:`lock.tryLock(1, TimeUnit.SECONDS)` 表示**最多等待 1 秒**,超时还没拿到锁就返回 false——对应 Demo02 注释"拿不到锁了不会再傻等" > - **中断异常处理**:tryLock(timeout) 会抛 `InterruptedException`,需 try-catch(这里抛 RuntimeException 简化) ### LockDemo 四种用法 —— 进阶 3:lockInterruptibly() 可中断获取锁 ```java // 来源:course/LockDemo.java(lockInterruptibly 用法) //--------进阶用法:可中断的获取锁(和sleep中断机制配合) public void interruptLock(){ try { lock.lockInterruptibly(); //等待所的过程中可以被 Interrupt()打断 try { count++; }finally { lock.unlock(); } } catch (InterruptedException e) { System.out.println("等待锁的过程中被中断"); Thread.currentThread().interrupt(); } } ``` > **要点**: > - **lockInterruptibly() 可中断**:在**等待锁的过程中可以被 interrupt() 打断**(对应 Demo02 注释"可以响应中断")——与 08-10 的 interrupt 中断机制配合 > - **catch 重设标志**:被中断时打印提示并 `Thread.currentThread().interrupt()` 重设中断标志(回顾 08-10 中断机制最佳实践) > - **与 tryLock 区别**:tryLock 是"拿不到就算了";lockInterruptibly 是"继续等,但可以被中断"——都是避免死锁/响应中断的手段 ### LockDemo main 演示 —— 10000 线程 count 验证 ```java // 来源:course/LockDemo.java(main 方法) public static void main(String[] args) throws InterruptedException { LockDemo ld=new LockDemo(); Thread[] threads=new Thread[10000]; for (int i = 0; i < threads.length; i++) { threads[i]= new Thread(ld::interruptLock); threads[i].start(); } //等待所有线程结束 for (Thread t:threads){ t.join(); } //理论上应该是1000,但是实际上往往小于1000 System.out.println("最终 count="+ld.getCount()); } ``` > **要点**: > - **10000 线程 + Lock 加锁**:10000 个线程并发执行 `ld::interruptLock`(方法引用,等价于 `() -> ld.interruptLock()`),用 lockInterruptibly + finally unlock 保护 count++——**保证原子性**,count 必然是 10000 > - **join 等待全部线程**:`t.join()` 等 10000 个线程全部结束后打印(回顾 08-10 join) > - **注释注意**:注释写着"理论上应该是1000"是沿用了上节课 Counter 的注释,实际 threadCount 是 10000——重点体会"加了 Lock 后 count 正确"的效果 > - **方法引用**:`ld::interruptLock` 是方法引用(回顾 08-04),等价于 Lambda `() -> ld.interruptLock()` --- ## 8. 死锁:产生条件与避免方法(Demo03) ### 概念 —— 什么是线程死锁 **线程死锁**指的是由**两个或多个线程相互持有对方所需要的资源**,导致这些线程处在**等待状态,无法继续执行**。 ```java // 来源:course/Demo03.java(类注释) /* 线程死锁指的是由两个或多个线程相互持有对方所需要的资源,导致这些线程处在等待状态,无法继续执行。 什么情况会出现死锁? 资源有限 同步嵌套 */ ``` | 死锁出现场景 | 说明 | |--------------|------| | **资源有限** | 多个线程竞争有限的资源(锁/连接等),资源不够分 | | **同步嵌套** | 一个同步块里嵌套了另一个同步块(持有锁 A 再等锁 B),多个线程嵌套方向相反就形成死锁 | > **要点**: > - **死锁的本质**:多个线程**互相持有对方需要的资源**,大家都在等对方释放,谁也无法继续 > - **两个典型场景**:①资源有限(竞争同一批稀缺资源)②同步嵌套(锁中套锁、嵌套方向相反) ### 死锁的 4 个必要条件(必须同时满足) ```java // 来源:course/Demo03.java(类注释) /* 出现死锁的条件: 1、互斥 资源同一时刻只能被同一个线程占用, 锁A/锁B只能被一个线程持有 2、持有且等待 线程持有一个资源,同时等待另一个资源 线程1持有锁A,还在等锁B 3、不可剥夺 已经持有的资源不能被强行夺走,只能自己释放,锁必须有持有者主动释放 4、循环等待 多个线程形成了 你等我,我等你的环, 线程1等锁B,线程2等锁A */ ``` | # | 必要条件 | 说明 | |---|----------|------| | 1 | **互斥** | 资源同一时刻只能被**同一个线程**占用(锁 A / 锁 B 只能被一个线程持有) | | 2 | **持有且等待** | 线程**持有一个资源,同时等待另一个资源**(线程 1 持有锁 A,还在等锁 B) | | 3 | **不可剥夺** | 已持有的资源**不能被强行夺走**,只能自己释放(锁必须有持有者主动释放) | | 4 | **循环等待** | 多个线程形成"**你等我、我等你**"的环(线程 1 等锁 B,线程 2 等锁 A) | > **要点**: > - **4 个条件缺一不可**:互斥 + 持有且等待 + 不可剥夺 + 循环等待——只要破坏其中一个,死锁就能避免 > - **循环等待是最后的环**:前 3 个是资源使用的基本规则,第 4 个"循环等待"是多个线程**互相等待形成闭环**,直接导致死锁 > - **破坏思路**:要避免死锁,就从打破这 4 个条件入手——比如打破"循环等待"(统一锁顺序)、打破"持有且等待"(tryLock 不无限等) ### 死锁演示 —— 线程 1 拿 lockA 等 lockB、线程 2 拿 lockB 等 lockA ```java // 来源:course/Demo03.java(死锁演示 main 方法,注释保留) public static void main(String[] args) { //线程1 先拿lockA,再拿lockB Thread t1 = new Thread(() -> { synchronized (lockA) { System.out.println("线程1 拿到 lockA"); try { Thread.sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); } synchronized (lockB) { //等待lockB(被线程2持有) System.out.println("线程1 拿到LockB"); } } }, "线程1"); Thread t2 = new Thread(() -> { synchronized (lockB) { System.out.println("线程2 拿到 lockB"); try { Thread.sleep(10000); } catch (InterruptedException e) { throw new RuntimeException(e); } synchronized (lockA) { //等待lockA(被线程1持有) System.out.println("线程2 拿到LockA"); } } }, "线程2"); t1.start(); t2.start(); } ``` ### 死锁过程拆解(经典锁顺序冲突) ``` 线程1:拿 lockA ✅ → sleep(100) → 等 lockB(被线程2持有)❌ 永远等不到 线程2:拿 lockB ✅ → sleep(10000) → 等 lockA(被线程1持有)❌ 永远等不到 → 线程1 持有 lockA 等 lockB,线程2 持有 lockB 等 lockA → 循环等待 → 死锁 ``` > **要点**: > - **加锁顺序相反**:线程 1 按"**lockA → lockB**"顺序,线程 2 按"**lockB → lockA**"顺序——两者**顺序相反**是死锁的直接原因 > - **sleep 放大死锁**:两个线程 sleep 后仍持有第一把锁,让"等第二把锁"的机会更充分,便于复现死锁 > - **满足 4 个条件**:互斥(两把锁各被一个线程持有)+ 持有且等待(各持一把等另一把)+ 不可剥夺(锁只能主动释放)+ 循环等待(线程 1 等 lockB、线程 2 等 lockA)——4 条件全满足 → 死锁 > - **验证死锁**:运行后程序**卡住不结束**,只打印"线程1 拿到 lockA"和"线程2 拿到 lockB",再也打印不出"拿到 LockB / 拿到 LockA" ### 避免死锁方法 1 —— 固定锁顺序(所有线程按相同顺序加锁) ```java // 来源:course/Demo03.java(固定锁顺序演示 main 方法,注释保留) public static void main(String[] args) { Runnable task = () -> { synchronized (lockA) { //所有线程统一先拿 lockA System.out.println(Thread.currentThread().getName() + " 拿到 lockA"); try { Thread.sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); } synchronized (lockB) { //再拿 lockB——顺序统一 System.out.println(Thread.currentThread().getName() + " 拿到 LockB"); } } }; Thread t1 = new Thread(task, "线程1"); Thread t2 = new Thread(task, "线程2"); t1.start(); t2.start(); } ``` > **要点**: > - **核心思想**:所有线程**都按照相同的顺序进行加锁**(统一 lockA → lockB),就不会出现"线程 1 等 lockB、线程 2 等 lockA"的循环等待 > - **打破"循环等待"条件**:固定锁顺序破坏了死锁第 4 个必要条件(循环等待)——大家排队按同一顺序拿锁,永远不会形成等待环 > - **写法区别**:死锁演示是两个线程各写各的(顺序相反);这里两个线程**共用同一个 task**(顺序统一)——对比很直观 ### 避免死锁方法 2 —— tryLock() 超时获取(拿不到就放弃) ```java // 来源:course/Demo03.java(tryLock 超时演示 main 方法核心逻辑,注释保留) private static final ReentrantLock rLockA = new ReentrantLock(); private static final ReentrantLock rLockB = new ReentrantLock(); public static void main(String[] args) { //线程1 先拿lockA,再拿lockB Thread t1 = new Thread(() -> { try { if (rLockA.tryLock(1, TimeUnit.SECONDS)) { //尝试获取lockA,等1秒 try { System.out.println("线程1 拿到 lockA"); Thread.sleep(100); if (rLockB.tryLock(1, TimeUnit.SECONDS)) { //等待lockB,等1秒超时放弃 try { System.out.println("线程1 拿到LockB"); } finally { rLockB.unlock(); //释放锁 } } else { System.out.println("线程1 拿不到锁"); //超时放弃,不无限等待 } } finally { rLockA.unlock(); //释放锁 } } } catch (InterruptedException e) { throw new RuntimeException(e); } }, "线程1"); // 线程2 逻辑与线程1对称(先拿rLockB,再拿rLockA),也全部用 tryLock(1, SECONDS) ... t1.start(); t2.start(); } ``` ### tryLock 超时避免死锁的原理 ``` 线程1:tryLock(rLockA) ✅ → sleep(100) → tryLock(rLockB, 1秒) 线程2:tryLock(rLockB) ✅ → sleep(100) → tryLock(rLockA, 1秒) → 线程1 等 rLockB 最多等 1 秒:拿不到就打印"拿不到锁",并释放 rLockA → 不死锁 → 线程2 同理:拿不到就放弃,释放 rLockB → 不死锁 ``` > **要点**: > - **核心思想**:`tryLock(timeout)` **超时获取,拿不到就放弃,而不是无限等待**——对应第 7 节 Lock 进阶用法 > - **打破"持有且等待"条件**:tryLock 超时后**释放自己已持有的锁**(finally 里 unlock)并继续执行别的,破坏了死锁第 2 个必要条件(持有且等待) > - **与死锁演示的对比**:死锁演示用 `synchronized`(不可超时,无限等待);这里改用 `ReentrantLock.tryLock(1, TimeUnit.SECONDS)`——就算等不到也**最多等 1 秒**就放弃,程序不会卡死 > - **finally 释放锁**:每把锁拿到后都在 finally 里 unlock——即使拿不到第二把锁,也要先释放第一把(铁律,回顾第 7 节) ### 避免死锁方法 3 —— 使用高级并发工具 ```java // 来源:course/Demo03.java(类注释) /* 3、使用更加高级的并发工具,避免手动加多把锁,比如使用一些同步锁的API 比如同步Map集合-->ConcurrentHashMap */ ``` | 避免方法 | 打破的条件 | 说明 | |----------|-----------|------| | **固定锁顺序** | 打破"循环等待" | 所有线程按相同顺序加锁,不会形成等待环 | | **tryLock() 超时获取** | 打破"持有且等待" | 拿不到就放弃并释放已持有的锁,不无限等待 | | **高级并发工具** | 从源头避免手动加多把锁 | 用 JDK 提供的线程安全容器(如 `ConcurrentHashMap` 同步 Map 集合),避免手动加多把锁 | > **要点**: > - **三种避免方法对应打破不同条件**:固定锁顺序 → 打破循环等待;tryLock 超时 → 打破持有且等待;高级工具 → 从源头不手动加多把锁 > - **高级工具示例**:**`ConcurrentHashMap`(同步 Map 集合)**——JDK 提供的线程安全并发容器,内部已经处理好线程安全,不需要自己手动加多把锁 > - **生产实践**:真实项目里少手动管理多把锁,优先用线程池 + 并发容器(ConcurrentHashMap / CopyOnWriteArrayList)+ 原子类等 JDK 组件 --- ## 9. 线程池:ThreadPoolExecutor(Demo04 / ThreadPoolDemo) ### 概念 —— 为什么需要线程池(new Thread().start() 的三个问题) **线程池(Thread Pool)**也是一种容器——**存储线程的容器**(就像水池是存储水的容器)。每次都 `new Thread().start()` 创建线程、运行后销毁,**创建线程的过程成本很高**(涉及和底层操作系统的交互)。当程序需要创建大量**生存期非常短**的线程时,频繁的创建和销毁会**耗费大量资源**。 ```java // 来源:course/Demo04.java(类注释) /* 说到池,第一想到是水池,水池是个容器,存储水的。 线程池也是一种容器,存储线程的。 每次都使用start() 创建线程,然后运行然后销毁线程。创建线程的过程的成本是比较高的,涉及到和底层操作系统的交互。 当程序中需要创建大量的生存期非常短的线程,频繁的线程创建和销毁就会耗费大量的资源。 所以就有了线程池 new Tread().star() 会带来三个问题: 1、创建/销毁线程的开销很大:每个线程都要分配1MB的栈内存资源,还有和底层操作系统进行交互,频繁的创建和销毁会拖垮性能。 2、线程的数量不受控:高并发的时候可能会瞬间创建成千上万个线程,耗尽内存和CPU,导致系统崩溃。 3、资源利用率低:可能一个任务只需要执行几毫秒,但是创建/销毁需要几秒线程的开销比业务本身还要大。 */ ``` ### new Thread().start() 的三个问题 | # | 问题 | 说明 | |---|------|------| | 1 | **创建/销毁开销很大** | 每个线程都要分配 **1MB 栈内存**资源,还要和底层操作系统交互——频繁创建和销毁会**拖垮性能** | | 2 | **线程数量不受控** | 高并发时可能**瞬间创建成千上万个线程**,耗尽内存和 CPU,导致**系统崩溃** | | 3 | **资源利用率低** | 一个任务可能只需执行**几毫秒**,但创建/销毁线程需要**几秒**——线程的开销比业务本身还大 | > **要点**: > - **线程创建成本高**:涉及底层操作系统交互 + 每线程 1MB 栈内存——`new Thread().start()` 不是"免费"的 > - **三个问题的本质**:开销大(创建销毁贵)、不受控(数量失控)、利用率低(创建时间 >> 执行时间) > - **线程池正是为解决这三个问题而生**:提前创建线程并常驻,复用而不重复创建 ### 线程池解决方案 —— 线程复用 + 任务排队 + 动态扩容 + 上限保护 ```java // 来源:course/Demo04.java(类注释) /* 线程池(Thread Pool)的解决方案:提前创建一批线程并且常驻。任务来了直接复用空闲的线程,任务过多的时候排队等待。 排队也排不下再进行动态扩容。达到了上限后执行拒绝。 【线程复用+任务排队+动态扩容+上限保护】 */ ``` | 机制 | 说明 | |------|------| | **线程复用** | 提前创建一批线程并且常驻,任务来了**直接复用空闲线程** | | **任务排队** | 任务过多时**排队等待** | | **动态扩容** | 排队也排不下时**再进行动态扩容**(创建临时线程) | | **上限保护** | 达到上限后执行**拒绝策略** | > **要点**: > - **一句话记忆**:**【线程复用 + 任务排队 + 动态扩容 + 上限保护】**——这是线程池的四大机制 > - **核心思想**:不重复创建销毁线程,而是"线程常驻 + 任务分配"——任务来了复用空闲线程,忙不过来排队,排不下扩容,达到上限拒绝 ### 类比理解 —— 饭店餐馆模型 ```java // 来源:course/Demo04.java(类注释) /* 类比-饭店餐馆: 核心线程数:饭店里面摆好的几张餐桌,比如5个,不管忙不忙都在。 最大线程数:饭店额外存了几张临时餐桌,比如3个,当5个满的时候用临时的。 工作队列:饭店门口摆了5个小凳,当8个桌子都满了的时候,顾客就坐在小凳上排队等待。 空闲存活时间:超过某个时间点3个临时餐桌没人用就收回来。 拒绝策略:如果5+3+5都满了,服务员就拒绝。 */ ``` | 线程池参数 | 饭店类比 | 说明 | |-----------|----------|------| | **核心线程数** | 摆好的几张固定餐桌(5 个) | 不管忙不忙**都在** | | **最大线程数** | 额外存的临时餐桌(3 个) | 核心桌满时用临时的 | | **工作队列** | 门口排队的小凳(5 个) | 所有桌子都满时顾客排队等待 | | **空闲存活时间** | 临时餐桌没人用就收回来 | 超过时间点临时资源被回收 | | **拒绝策略** | 5+3+5 都满了服务员拒绝 | 全部满了就拒绝新顾客(任务) | > **要点**: > - **类比记忆五要素**:核心线程 = 固定餐桌、最大线程 = 临时餐桌、工作队列 = 门口排队凳、空闲存活时间 = 临时桌收回、拒绝策略 = 服务员拒绝 > - **线程池参数和饭店运营一一对应**——把抽象参数变成生活场景就很好记 ### ThreadPoolExecutor 的 7 个参数 ```java // 来源:course/Demo04.java(类注释) /* corePoolSize 核心线程数 即使是空闲也要保留 maximumPoolSize 最大线程数 必须要大于核心线程数 keepAliveTime 非核心线程空闲存活时间 默认只对非核心线程有效 unit 时间单位 存活时间的单位 workQueue 工作队列 排队的地方 threadFactory 线程工厂 创建线程的工厂 handler 拒绝策略 如果排不下了怎么办 */ ``` | 参数 | 说明 | |------|------| | **corePoolSize** | **核心线程数**——即使空闲也保留 | | **maximumPoolSize** | **最大线程数**——必须大于核心线程数 | | **keepAliveTime** | **非核心线程空闲存活时间**——默认只对非核心线程有效 | | **unit** | **时间单位**——存活时间的单位(如 TimeUnit.SECONDS) | | **workQueue** | **工作队列**——排队的地方 | | **threadFactory** | **线程工厂**——创建线程的工厂(可自定义线程名、是否守护线程) | | **handler** | **拒绝策略**——如果排不下了怎么办 | > **要点**: > - **7 个参数逐个记**:核心线程数、最大线程数、空闲存活时间、时间单位、工作队列、线程工厂、拒绝策略 > - **maximumPoolSize > corePoolSize**:最大线程数**必须大于**核心线程数(= 核心 + 临时) > - **keepAliveTime 只对非核心线程有效**:空闲时间超过 keepAliveTime,非核心(临时)线程被回收,核心线程保留 ### 线程池的执行流程 ```java // 来源:course/Demo04.java(类注释) /* 线程池的执行流程: 核心线程-->工作队列-->非核心线程-->拒绝策略 线程不够了,不会第一时间创建新的线程,而是先把任务放在工作队列,只有当工作队列满了,并且线程数还没到最大线程数的时候,才会创建新的线程。 */ ``` ### 执行流程四阶段 ``` 任务来了 │ ▼ ① 核心线程 → 有空闲核心线程就执行,没有则下一步 │ ▼ ② 工作队列 → 放入队列排队等待(核心线程全忙时) │ ▼ ③ 非核心线程 → 队列满了且线程数 < 最大线程数,才创建临时线程 │ ▼ ④ 拒绝策略 → 队列满 + 线程数已达最大值,执行拒绝 ``` > **要点**: > - **流程一句话**:**核心线程 → 工作队列 → 非核心线程 → 拒绝策略** > - **关键认知**:线程不够了**不会第一时间创建新线程**,而是**先把任务放工作队列**;只有**工作队列满了**并且**线程数还没到最大线程数**时,才**创建新线程** > - **这与直觉相反**:先排队、后扩容——扩容(创建临时线程)是"最后的手段"之一 ### Demo04 main 演示 —— 创建线程池 ```java // 来源:course/Demo04.java(main 方法) public static void main(String[] args) { ThreadPoolExecutor pool = new ThreadPoolExecutor( 3, //corePoolSize-核心线程数 创建线程池的时候就先创建3个核心的线程,不管有没有任务使用,都留着。 5, //maximumPoolSize-最大线程数 核心线程数 + 临时线程数 60, //keepAliveTime-非核心线程空闲存活时间 TimeUnit.SECONDS, //unit-时间单位 new ArrayBlockingQueue<>(10), //workQueue-工作队列 容量为10 Executors.defaultThreadFactory(), //threadFactory-线程工厂,可以自定义线程名、是否为守护线程 new ThreadPoolExecutor.AbortPolicy() //handler-拒绝策略--AbortPolicy默认策略抛异常 ); } ``` > **要点**: > - **7 参数齐全示例**:3 核心 / 5 最大 / 60 秒存活 / SECONDS 单位 / ArrayBlockingQueue(10) 队列 / defaultThreadFactory 工厂 / AbortPolicy 拒绝策略 > - **ArrayBlockingQueue(10)**:基于数组的有界阻塞队列,容量 10——排队的"凳子"最多 10 个 > - **AbortPolicy(默认拒绝策略)**:队列满 + 线程满时**抛异常**拒绝新任务——拒绝策略之一 > - **threadFactory**:可用 `Executors.defaultThreadFactory()` 默认工厂;也可自定义(设置线程名、守护属性) ### ThreadPoolDemo 演示 —— 8 个任务观察线程池运行 ```java // 来源:course/ThreadPoolDemo.java(main 方法) public static void main(String[] args) { ThreadPoolExecutor pool = new ThreadPoolExecutor(2, 4, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()); for (int i = 1; i <= 8; i++) { int taskId = i; try { pool.execute(() -> { String name = Thread.currentThread().getName(); System.out.println(name + "执行任务" + taskId); try { Thread.sleep(2000); //模拟任务耗时,占住线程 } catch (InterruptedException e) { throw new RuntimeException(e); } }); } catch (RejectedExecutionException e) { System.out.println("任务" + taskId + "被拒绝" + e.getMessage()); } } //观察线程池的状态:活动线程数、队列的大小 System.out.println("活动线程数:" + pool.getActiveCount()); System.out.println("队列中任务数:" + pool.getQueue().size()); pool.shutdown(); //关闭线程池 } ``` ### ThreadPoolDemo 执行过程拆解(2 核心 4 最大 队列 2) ``` 配置:corePoolSize=2、maximumPoolSize=4、队列容量=2,连续提交 8 个任务 任务1、2 → 核心线程(2 个)立即执行 [核心线程 2 个满] 任务3、4 → 进入工作队列排队 [队列 2 个满] 任务5、6 → 队列满了 → 创建临时线程(非核心) [线程数 4 = 最大值] 任务7、8 → 线程满 + 队列满 → AbortPolicy 拒绝 → 抛 RejectedExecutionException 观察输出:活动线程数=4、队列中任务数=2 ``` > **要点**: > - **8 任务的走向**:2 个核心执行 + 2 个排队 + 2 个临时线程执行 + 2 个被拒绝——完整演示了"核心线程 → 工作队列 → 非核心线程 → 拒绝策略"四阶段流程 > - **getActiveCount()**:查看当前**活动线程数**(这里 2 核心 + 2 临时 = 4) > - **getQueue().size()**:查看**队列中排队任务数**(这里 2 个) > - **RejectedExecutionException**:被拒绝的任务抛这个异常,catch 后打印"任务 X 被拒绝" > - **shutdown()**:**关闭线程池**——不再接受新任务,已提交任务继续执行完 > - **execute() 与 start() 的区别**:`pool.execute(runnable)` 把任务交给线程池(复用线程),而不是 `new Thread().start()`(创建新线程)——这正是线程池的核心价值 --- ## 10. 课后作业回顾:synchronized 修复卖票问题(homework0810 Demo_03) ### 作业背景 `homework0810/Demo_03.java` 是 8 月 10 日课后作业的**第三题**:**用 synchronized 修复卖票问题**(回顾上节课 SellTicket 的数据竞争 bug)。本题给出**代码框架 + TODO 提示**,要求学生自己动手:先不加 synchronized 复现 bug,再加 synchronized 修复。 ### ① 卖票类:实现 Runnable(TODO 1、2) ```java // 来源:homework0810/Demo_03.java static class SellTicket implements Runnable { private int tickets = 100; // 一共 100 张票 // 写法1:同步方法(锁 this)—— TODO 2 完成后给方法加 synchronized public void sell() { // TODO 1: if (tickets > 0) { sleep(10); 打印 "xxx正在出售第 tickets 张票"; tickets--; } if (tickets > 0) { try { Thread.sleep(10); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(Thread.currentThread().getName()+"正在出售第 "+tickets+" 张票"); tickets--; } } @Override public void run() { while (tickets > 0) { synchronized (this){ sell(); // TODO 2: 先不加 synchronized 运行复现 Bug,再加 synchronized 修复 } } } } ``` ### ② 创建 3 个售票窗口线程(TODO 3、4、5) ```java // 来源:homework0810/Demo_03.java(main 方法) public static void main(String[] args) throws InterruptedException { // ========== ② 创建 3 个售票窗口线程(共用同一个 SellTicket 对象) ========== // TODO 3: new Thread(new SellTicket(), "窗口1") / "窗口2" / "窗口3",start() 启动 SellTicket st=new SellTicket(); //创建三个线程对象,模拟3个售票窗口 Thread t1=new Thread(st,"窗口1"); Thread t2=new Thread(st,"窗口2"); Thread t3=new Thread(st,"窗口3"); t1.start(); t2.start(); t3.start(); // TODO 4: 用线程数组 + join() 等待所有线程卖票结束 // TODO 5: 运行多次观察:无 synchronized 时出现相同票 / 负数票;加 synchronized 后消失 } ``` ### 作业拆解(TODO → 知识点对应) | TODO | 要求 | 对应知识点 | |------|------|-----------| | **TODO 1** | 完成卖票逻辑:`if (tickets>0) { sleep(10); 打印; tickets--; }` | 卖票问题数据竞争(相同票 / 负数票) | | **TODO 2** | 先不加 synchronized 运行复现 Bug,再加 synchronized 修复 | synchronized 线程同步(三种写法) | | **TODO 3** | 用同一个 `st` 创建 3 个窗口线程并 start() | 多线程共享同一对象 → 共享数据 tickets | | **TODO 4** | 用线程数组 + `join()` 等待所有线程卖票结束 | join() 等待线程结束(08-10) | | **TODO 5** | 多次运行观察:无 synchronized 相同票 / 负数票,加后消失 | 数据竞争现象验证 + synchronized 修复 | > **要点**: > - **同一个 SellTicket 对象**:三个线程 `new Thread(st, "窗口N")` **共用同一个 st** → 共享 tickets 数据 → 产生数据竞争(回顾 08-10 数据竞争三原因:多线程 + 共享数据 + 读-改-写复合操作) > - **加锁位置**:`synchronized(this)` 锁住 `sell()` 的"**判断-打印-减票**"整体过程——同一时刻只有一个窗口能执行卖票 > - **三种写法可替换**:作业框架用**同步代码块** `synchronized(this)`(写法 3);也可以给 `sell()` 加 `synchronized` 修饰(写法 1:同步实例方法锁 this),两种都有效 > - **join() 完善**:TODO 4 要求补上"线程数组 + join()"——让 main 等 3 个窗口卖完票再结束(回顾 08-10 join 等待线程结束) > - **验证思路**:先不加 synchronized 多次运行看 bug(相同票 / 负数票),加 synchronized 后再运行 bug 消失——用实践验证上节课"锁"的作用 --- ## 11. 知识点全景总结 本课知识点围绕"**volatile 解决可见性 / 有序性 + JVM 内存模型 + 线程安全三大特性解决方案 + Lock 锁(ReentrantLock)+ 死锁(产生条件与避免方法)+ 线程池(ThreadPoolExecutor)+ 课后作业(synchronized 修复卖票问题)**"展开,一张表看清"知识 → 代码位置": | 知识点 | 具体体现 | 源码位置 | |--------|----------|----------| | **volatile 概述** | 解决可见性和有序性问题,不能保证原子性;volatile 可解决"程序永不退出" | course/Demo01 注释 | | **可见性问题定义** | 一个线程对共享数据的修改,对其他线程不可见,其他线程无法做出相应响应 | course/Demo01 注释 | | **JVM 内存模型** | 每个线程有独立工作内存(CPU 缓存);读变量优先从工作内存读、写变量先写工作内存再刷主内存,刷新有延迟 | course/Demo01 注释 | | **可见性问题原理** | main 把 running 改 false 写主存,worker 一直读 CPU 缓存旧值 true → while(running) 永不退出 | course/Demo01 注释 | | **volatile 语义 1:可见性** | 写 volatile 变量立即刷主存,读 volatile 变量强制从主存读,禁止使用 CPU 缓存 | course/Demo01 注释 | | **volatile 语义 2:禁止重排序** | 编译器和 CPU 不会把 volatile 读写及其前后指令进行指令重排(有序性) | course/Demo01 注释 | | **volatile 语义 3:不保证原子性** | volatile count++ 依旧丢失更新,做不到 3 步变 1 步;必须 synchronized/Lock/原子类 | course/Demo01 注释 | | **可见性演示:程序永不退出** | running 加 volatile;worker 线程 while(running) 空循环,main 主线程 stop() 后能正常退出 | course/VolatileDemo | | **方法引用创建线程** | `new Thread(vd::work,"worker")` 等价于 Lambda `() -> vd.work()`、等价于匿名 Runnable | course/VolatileDemo main | | **三大特性解决方案回顾** | 原子性→synchronized/Lock/Atomic;可见性→volatile/synchronized/Lock;有序性→volatile/synchronized | course/Demo01 注释 | | **volatile vs synchronized** | volatile 轻量、只解决可见性/有序性;synchronized 加锁、同时保证三大特性 | course/Demo01 注释 | | **Counter 升级:volatile 不保证原子性** | `private volatile Integer count=0`——count++ 的"读-改-写"3 步依旧被打断,count 还是小于 1000 | course/Counter | | **Counter 升级:synchronized 保证原子性** | `public synchronized void increment()` 同步实例方法(锁 this),同一时刻只一个线程执行 count++ → count 必然是 1000 | course/Counter | | **1000 线程 + join 等待** | 线程数组 1000 个线程并发 increment(),`for (Thread t : threads) t.join()` 等全部结束后打印 | course/Counter main | | **课后作业:卖票类 SellTicket** | implements Runnable,tickets=100;sell() 完成"判断-打印-减票";run() 中 `synchronized(this)` 锁住卖票过程 | homework0810/Demo_03 | | **课后作业:3 个窗口线程** | 同一个 `st` 对象创建"窗口1/2/3"三个线程并 start()——共享 tickets 产生数据竞争 | homework0810/Demo_03 main | | **课后作业:synchronized 修复** | 先不加 synchronized 复现相同票/负数票,再加 synchronized(this) 修复(三种写法均可) | homework0810/Demo_03 TODO | | **课后作业:join 等待** | TODO 4 用线程数组 + join() 等所有窗口卖票结束 | homework0810/Demo_03 TODO | | **Lock 接口与 ReentrantLock** | JDK1.5 提供 `java.util.concurrent.locks.Lock` 接口及实现类 ReentrantLock(可重入锁);比 synchronized 更精细 | course/Demo02 注释 | | **Lock 相比 synchronized 的优势** | 超时等待(拿不到锁不傻等)、响应中断(等待中可被 interrupt() 中断)、指定公平性(按申请顺序)、支持多条件变量(精准唤醒) | course/Demo02 注释 | | **Lock 手动释放锁铁律** | synchronized 自动释放锁;Lock 必须手动 `unlock()` 且**必须放 finally**,否则抛异常/return 时锁不释放 → 其他线程永久阻塞(死锁) | course/Demo02 注释 | | **ReentrantLock 构造方法** | `new ReentrantLock()` 默认非公平锁(释放后所有线程竞争);`new ReentrantLock(true)` 公平锁(按申请顺序、性能略低) | course/Demo02 注释、LockDemo | | **Lock 核心方法** | `lock()` 加锁 / `unlock()` 释放锁 / `tryLock()` 尝试获取(获取不到就算了)/ `tryLock(timeout, unit)` 限时等待 / `lockInterruptibly()` **设置可中断的锁** | course/Demo02 注释 | | **synchronized vs Lock 七维对比** | 语法简洁度(关键字 vs try-finally)、锁释放(自动 vs 手动 unlock)、可中断(不支持 vs lockInterruptibly)、超时等待(不支持 vs tryLock(timeout))、**锁读(不支持 vs 读写分离单独上锁)**、**底层实现(Monitor Lock vs AQS 抽象队列同步器)**、**JDK21(synchronized 大幅优化、性能相近)** | course/Demo02 注释 | | **LockDemo 基本用法** | `lock.lock()` → `try { count++ } finally { lock.unlock() }` 标准模板,保证原子性 | course/LockDemo | | **LockDemo tryLock()** | `lock.tryLock()` 立刻尝试,拿到返回 true 并操作,没拿到返回 false 继续干别的——**避免死锁的手段** | course/LockDemo | | **LockDemo tryLock(timeout)** | `lock.tryLock(1, TimeUnit.SECONDS)` 最多等 1 秒,超时返回 false;抛 InterruptedException 需 try-catch | course/LockDemo | | **LockDemo lockInterruptibly()** | 等待锁的过程中可被 interrupt() 打断;catch 中打印提示并 `Thread.currentThread().interrupt()` 重设中断标志 | course/LockDemo | | **LockDemo main 演示** | 10000 线程并发 `ld::interruptLock`(方法引用),join 等待后打印 count——Lock 保证原子性,count 正确 | course/LockDemo main | | **死锁概念** | 两个或多个线程相互持有对方所需资源,导致线程处在等待状态无法继续执行;出现在资源有限 / 同步嵌套场景 | course/Demo03 注释 | | **死锁 4 个必要条件** | ①互斥(资源同一时刻只被一个线程占用)②持有且等待(持有一个资源同时等另一个)③不可剥夺(已持有的资源不能被强夺)④循环等待(你等我、我等你的环)——缺一不可 | course/Demo03 注释 | | **死锁演示** | 线程1 拿 lockA 等 lockB、线程2 拿 lockB 等 lockA(synchronized 嵌套顺序相反)→ 4 条件满足 → 死锁卡死 | course/Demo03 main(注释) | | **避免死锁 1:固定锁顺序** | 所有线程按相同顺序加锁(统一 lockA → lockB),打破"循环等待"条件 | course/Demo03 main(注释) | | **避免死锁 2:tryLock 超时** | `tryLock(1, TimeUnit.SECONDS)` 拿不到就放弃并 finally 释放已持锁,打破"持有且等待"条件 | course/Demo03 main(注释) | | **避免死锁 3:高级并发工具** | 用 `ConcurrentHashMap` 同步 Map 集合等 JDK 并发容器,避免手动加多把锁 | course/Demo03 注释 | | **线程池概念** | 线程池是存储线程的容器(水池类比);new Thread().start() 创建线程成本高(涉及操作系统交互) | course/Demo04 注释 | | **new Thread().start() 三个问题** | ①创建/销毁开销大(每线程 1MB 栈内存 + 操作系统交互)②线程数量不受控(高并发耗尽内存 CPU 崩溃)③资源利用率低(任务几毫秒、创建销毁几秒) | course/Demo04 注释 | | **线程池解决方案** | 【线程复用 + 任务排队 + 动态扩容 + 上限保护】——提前创建线程常驻,任务复用空闲线程,排不下动态扩容,达到上限拒绝 | course/Demo04 注释 | | **饭店餐馆类比** | 核心线程数=固定餐桌、最大线程数=临时餐桌、工作队列=门口排队凳、空闲存活时间=临时桌收回、拒绝策略=服务员拒绝 | course/Demo04 注释 | | **ThreadPoolExecutor 7 参数** | corePoolSize(核心线程数)/ maximumPoolSize(最大线程数,必须 > 核心)/ keepAliveTime(非核心空闲存活时间)/ unit(时间单位)/ workQueue(工作队列)/ threadFactory(线程工厂)/ handler(拒绝策略) | course/Demo04 注释 | | **线程池执行流程** | 核心线程 → 工作队列 → 非核心线程 → 拒绝策略;线程不够不先创建线程,先排队,队列满且未达最大才扩容 | course/Demo04 注释 | | **Demo04 创建线程池** | new ThreadPoolExecutor(3, 5, 60, SECONDS, ArrayBlockingQueue(10), defaultThreadFactory, AbortPolicy)——7 参数齐全 | course/Demo04 main | | **ThreadPoolDemo 演示** | 8 任务 2 核心 4 最大 队列 2:2 执行 + 2 排队 + 2 临时线程 + 2 被拒绝;getActiveCount / getQueue().size / shutdown | course/ThreadPoolDemo main | --- ## 12. 随堂练习要点 - **程序永不退出实验(必做)**:把 `VolatileDemo` 中 `running` 的 `volatile` 去掉,运行观察 worker 线程是否**永远打印不出"线程退出......"**;加上 volatile 后再运行对比——直观体会可见性问题 - **CPU 缓存类比理解**:把"工作内存 = CPU 缓存"记牢——线程读变量**优先读自己的缓存**,刷新到主内存**有延迟**,这就是其他线程看不到修改的根本原因 - **volatile 三大语义背诵**:①可见性(写立即刷主存、读强制从主存读)②禁止重排序(volatile 读写及前后指令不重排)③不保证原子性(count++ 3 步依旧会丢更新) - **Counter 升级实验**:运行 `course/Counter`(count 加了 volatile、increment 加了 synchronized),多次运行确认 **count 必然是 1000**——对比上节课"无锁 count 小于 1000",体会 synchronized 解决原子性的效果 - **volatile vs 无 volatile 对比实验**:把 Counter 中 increment() 的 synchronized 去掉(保留 count 的 volatile),运行观察 count **依然小于 1000**——直接验证"volatile 不能保证原子性,必须 synchronized" - **三大特性方案对齐**:把"原子性→synchronized/Lock/Atomic、可见性→volatile/synchronized/Lock、有序性→volatile/synchronized"三个对应关系背下来 - **课后作业:卖票问题修复(必做)**:补全 `homework0810/Demo_03` 的 TODO——①完成 sell() 卖票逻辑 ②先不加 synchronized 运行复现相同票/负数票 ③加 synchronized(this) 修复 ④用线程数组 + join() 等待卖票结束 ⑤多次运行验证 bug 消失 - **三种写法替换练习**:把作业框架的 `synchronized(this) { sell(); }`(写法 3 同步代码块)改成 `sell()` 方法加 synchronized(写法 1 同步实例方法锁 this),运行验证同样有效——回顾 synchronized 三种写法 - **volatile 有序性思考**:思考为什么编译器和 CPU 要重排指令(性能优化)——volatile 禁止重排是在"性能"和"有序性"之间做取舍,保证多线程下代码按书写顺序执行 - **LockDemo 运行验证(必做)**:运行 `course/LockDemo`,10000 线程并发调用 interruptLock(),join 等待后观察 count 是否正确(必然 10000)——体会 Lock 锁保证原子性的效果 - **Lock 四种用法替换实验**:把 LockDemo 的 main 中 `ld::interruptLock` 分别换成 `ld::increment`(lock+unlock 基本用法)、`ld::tryIncrement`(tryLock 尝试获取)、`ld::timedIncrement`(tryLock 限时等待),运行对比四种用法的效果 - **tryLock 避免死锁实验**:调用 `tryIncrement()` 打印返回值,观察当多个线程竞争锁时,拿不到锁的线程返回 false、继续执行而不阻塞——体会 tryLock 是"避免死锁的手段" - **lockInterruptibly 中断实验**:一个线程执行 `interruptLock()`(若锁被占用则等待),main 里对该线程 `interrupt()`,观察 catch 中打印"等待锁的过程中被中断"并重设中断标志——体会 Lock 响应中断的能力(对比 synchronized 不支持) - **synchronized vs Lock 对比练习**:把上节课 SynchronizedDemo(synchronized 三种写法)和本课 LockDemo(Lock 四种用法)对照着看——同是解决原子性,一个自动释放锁、一个手动释放锁(必须 finally) - **unlock 必须放 finally 的思考**:思考如果 `lock.lock()` 后不用 try-finally,而是直接 `count++; lock.unlock();`,当 count++ 抛异常时会发生什么——锁永不释放,其他线程永久阻塞(死锁) - **公平锁与非公平锁对比实验**:分别用 `new ReentrantLock()`(非公平)和 `new ReentrantLock(true)`(公平)创建锁,观察竞争线程获取锁的顺序差异——公平锁按申请顺序、性能略低 - **七维对比表背诵**:把 Demo02 新增的 synchronized vs ReentrantLock **七维对比**记下来——语法简洁度 / 锁释放 / 是否可中断 / 是否可以超时等待 / 是否支持锁读(读写分离)/ 底层实现(Monitor Lock vs AQS)/ JDK21 版本更新(性能相近)——面试常考 - **锁读(读写分离)思考**:Demo02 提到 ReentrantLock"属于读写分离,单独上锁"——思考同步的粒度:读写分离意味着读锁和写锁可以分开控制(后续将学习 ReentrantReadWriteLock),读读之间可以并发,读写之间才互斥,性能更高 - **底层实现对比记忆**:synchronized 底层是**监视器锁(Monitor Lock)**;ReentrantLock 底层是 **AQS(AbstractQueuedSynchronizer)抽象队列同步器**——理解"锁的实现机制不同"是两种锁最本质的区别 - **JDK21 优化实验**:了解 JDK21 中 synchronized 进行了**大幅优化**、与 ReentrantLock **性能相近**——思考为什么简单场景优先用 synchronized(语法简单 + 性能不输 + 自动释放锁) - **死锁复现实验(必做)**:把 `Demo03` 的**死锁演示 main** 恢复运行,观察只打印"线程1 拿到 lockA"和"线程2 拿到 lockB"后**程序卡死**——验证死锁(4 个必要条件同时满足) - **jstack 观察死锁**:死锁运行时,用 **`jstack <进程号>`** 命令查看线程转储,找到 `Found one Java-level deadlock` 信息——生产环境排查死锁的标准手段 - **固定锁顺序修复实验**:把死锁演示改成 Demo03 的**固定锁顺序版**(两个线程共用同一个 task,统一 lockA → lockB),运行观察不再卡死——体会打破"循环等待"条件 - **tryLock 超时修复实验**:把死锁演示改用 **`tryLock(1, TimeUnit.SECONDS)`**(Demo03 第二种修复版),运行观察拿不到锁的线程打印"拿不到锁"后继续执行,不再死锁——体会打破"持有且等待"条件 - **4 条件分析练习**:对照死锁演示,逐个分析它满足哪 4 个必要条件(互斥 / 持有且等待 / 不可剥夺 / 循环等待),再思考"固定锁顺序"和"tryLock 超时"分别破坏了哪个条件 - **高级工具避免死锁思考**:了解 `ConcurrentHashMap`(同步 Map 集合)等 JDK 并发容器——为什么用它们可以避免手动加多把锁导致的死锁(容器内部已处理好线程安全) - **线程池 7 参数背诵(必做)**:把 ThreadPoolExecutor 的 7 个参数背下来——corePoolSize(核心线程数)/ maximumPoolSize(最大线程数)/ keepAliveTime(非核心空闲存活时间)/ unit(时间单位)/ workQueue(工作队列)/ threadFactory(线程工厂)/ handler(拒绝策略) - **执行流程记忆**:线程池执行流程四阶段——**核心线程 → 工作队列 → 非核心线程 → 拒绝策略**;关键认知:线程不够不先创建新线程,先放工作队列,队列满且未达最大线程数才扩容 - **ThreadPoolDemo 运行验证(必做)**:运行 `course/ThreadPoolDemo`,观察 8 个任务的走向——2 个核心执行 + 2 个排队 + 2 个临时线程 + 2 个被拒绝(抛 RejectedExecutionException),以及"活动线程数:4 / 队列中任务数:2"的输出 - **参数修改实验**:把 ThreadPoolDemo 的 corePoolSize 改成 4、队列改成 4,重新提交 8 个任务,观察拒绝的任务数量变化(8 = 4 核心 + 4 排队,全部执行无人被拒)——体会 7 参数如何影响执行流程 - **饭店类比复述**:用"饭店餐馆"模型复述线程池 5 要素(核心线程=固定餐桌 / 最大线程=临时餐桌 / 工作队列=排队凳 / 空闲存活=临时桌收回 / 拒绝策略=服务员拒绝)——抽象参数变生活场景 - **execute 与 start 对比实验**:对比 `pool.execute(runnable)`(复用线程池线程)与 `new Thread(runnable).start()`(每次创建新线程)——用 getName() 观察线程池中线程是否重复使用(多个任务共用 2 个核心线程名) - **shutdown 实验**:把 `pool.shutdown()` 注释掉再运行,观察程序是否**一直不退出**(线程池的非守护线程还活着)——体会 shutdown 关闭线程池的作用 - **拒绝策略实验**:把 AbortPolicy 换成 `CallerRunsPolicy`(调用者运行)或 `DiscardPolicy`(丢弃),提交超出容量的任务观察不同拒绝行为 --- ## 13. 拓展阅读 - **JDK 文档**:[`java.lang.Thread`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Thread.html)(线程类核心方法)、[`java.lang.Object`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Object.html)(wait/notify)、[`java.lang.Thread.State`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Thread.State.html)(线程 6 种状态枚举) - **volatile 的本质**:volatile 修饰的变量**不缓存到 CPU 寄存器/缓存**,读写都走**主内存**——这是它保证可见性的底层机制;Java 通过**内存屏障(Memory Barrier)**实现 volatile 的可见性与禁止重排序语义 - **volatile 不保证原子性**:`volatile count++` 中,虽然 count 读写走主内存,但 **count++ 的"读-改-写"3 步依旧可能被并发打断**(读旧值→计算→覆盖),所以 volatile **不能**替代 synchronized / 原子类解决原子性问题 - **volatile 的适用场景**:①一个线程写、其他线程读的**标志位**(如 VolatileDemo 的 running、开关标志);②**双检锁单例模式**(DCL)中修饰实例字段,防止指令重排导致拿到未初始化完成的对象 - **volatile 不适用于复合操作**:volatile 适合"**单一变量的可见性**"场景;对 `count++`、`tickets--` 这类**读-改-写复合操作**,必须用 synchronized / Lock / 原子类(AtomicInteger 等) - **synchronized 与 volatile 对比**:synchronized 是**锁机制**(Monitor Lock),同时保证原子性 + 可见性 + 有序性,但**重量级**、线程多时竞争锁耗资源;volatile 是**轻量级**同步,只保证可见性 + 有序性,**不加锁、不阻塞线程**——能用 volatile 的场景优先用 volatile(性能更好) - **指令重排(Reordering)**:编译器和 CPU 为了提升性能会**重排指令执行顺序**(在单线程下不改变结果);多线程下重排可能导致"看似不可能的顺序"出现——volatile 通过**内存屏障**禁止重排 volatile 读写及其前后指令,保证**有序性** - **可见性问题的其他解法**:除了 volatile,`synchronized` 和 `Lock` 也保证可见性(**加锁解锁会同步工作内存与主内存**);用 `Thread.join()`、`FutureTask.get()` 等"等待线程结束"的手段也能建立 happens-before 关系,间接保证可见性 - **happens-before 原则**:Java 内存模型(JMM)通过 **happens-before 规则**保证内存可见性——volatile 读写、synchronized 加锁/解锁、线程 start()/join() 等都是典型规则,深入理解 JMM 是并发编程进阶的必修课 - **原子类(java.util.concurrent.atomic)**:`AtomicInteger` / `AtomicLong` 等原子类内部用 **CAS(比较并交换)** 实现原子操作——`incrementAndGet()` 是原子的,无需加锁即可保证线程安全,性能优于 synchronized - **JMM 与操作系统内存模型**:JVM 的"工作内存"抽象对应操作系统/硬件层面的 **CPU 缓存(L1/L2/L3)** 与 **主内存(RAM)**;多核 CPU 下各核心缓存不一致,正是可见性问题的硬件根源 - **课后作业串联**:`homework0810/Demo_03` 把 08-10 的**卖票问题(数据竞争)+ synchronized(三种写法)+ join(等待线程结束)** 串成一道综合作业——先复现 bug、再加锁修复、最后 join 等待,完整实践"发现问题 → 分析原因 → 锁解决"的线程安全排错流程 - **多线程排错思路**:可见性/原子性类 bug **错误无法复现**(每次运行结果不同),排查常用思路——**缩小并发窗口**(加大 sleep 放大 bug)、**线程转储(thread dump)**、**打印中间状态**;修复手段按需选择:标志位→volatile,复合操作→synchronized/原子类 - **Lock 接口与 ReentrantLock**:[`java.util.concurrent.locks.Lock`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/locks/Lock.html) 接口与 [`ReentrantLock`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/locks/ReentrantLock.html)(可重入锁)是 JDK1.5 提供的显式锁机制;相比 synchronized 提供超时等待、响应中断、公平锁、多条件变量等精细控制 - **synchronized 与 Lock 的选择**:一般场景优先用 **synchronized**(自动释放锁、语法简单、可重入);需要**超时等待 / 响应中断 / 公平锁 / 多条件变量**等精细控制时用 **Lock**(ReentrantLock);Lock 使用必须**手动 unlock 并放 finally**,否则死锁 - **Lock 避免死锁的三板斧**:①`tryLock()` 获取不到就放弃(不阻塞)②`tryLock(timeout)` 限时等待(超时放弃)③`lockInterruptibly()` 可中断(等待中被 interrupt 打断)——三者都比 `lock()`(无限期等待)更安全 - **公平锁 vs 非公平锁**:非公平锁(默认)释放后所有线程竞争,性能高但可能"插队"(饿死等待久的线程);公平锁按申请顺序获取锁,性能略低但更公平——`new ReentrantLock(true)` 创建公平锁 - **Lock 与 wait/notify 的升级**:Lock 配合 `Condition` 对象可实现**多条件变量精准唤醒**(`condition.await()` / `condition.signal()`),是对 Object 类 wait/notify 的升级(synchronized 只有单一条件队列)——后续生产者-消费者高级模式会用到 - **可重入锁(Reentrant)的含义**:ReentrantLock 和 synchronized 一样是**可重入**的——同一线程可以重复获取自己已经持有的锁(如递归调用、锁内再调锁内方法),不会自己把自己锁死;锁关联持有计数,解锁次数要与加锁次数匹配 - **Lock 与 Atomic 类的选择**:解决原子性有**三条路**——synchronized(关键字简单可靠)、Lock(ReentrantLock 精细控制)、原子类(AtomicInteger 的 CAS 无锁实现性能最优)——按场景选择:简单场景 synchronized、复杂并发控制 Lock、纯计数/累加用原子类 - **AQS(AbstractQueuedSynchronizer)**:ReentrantLock 的底层核心是 **AQS 抽象队列同步器**——一个基于 **FIFO 等待队列** + **state 状态位** + **CAS** 实现的同步框架,ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch 等都基于它构建;synchronized 的底层则是**监视器锁(Monitor Lock)**(08-10 已学)——这是两种锁最本质的机制区别 - **读写锁(ReentrantReadWriteLock)**:Demo02 提到 ReentrantLock"属于读写分离,单独上锁"——进一步可了解 **ReentrantReadWriteLock**:读锁(共享锁,读读并发)+ 写锁(排他锁,读写/写写互斥),在读多写少场景显著提升并发性能 - **JDK21 对 synchronized 的优化**:JDK21 中 synchronized 进行了**大幅度的优化**,与 ReentrantLock **性能相近**——包括锁升级(偏向锁→轻量级锁→重量级锁)、锁消除、锁粗化等 JIT 优化手段;这意味着简单场景优先用 synchronized 的结论更加成立(语法简单 + 性能不输 + 自动释放锁) - **锁升级(Synchronized 优化原理)**:synchronized 的性能优化核心是**锁升级机制**——无锁 → 偏向锁(单线程竞争)→ 轻量级锁(CAS 自旋)→ 重量级锁(Monitor 阻塞);低竞争时偏向/轻量级锁开销极小,高竞争时才升级为重量级锁,这正是 JDK21 大幅优化的底层原因 - **死锁的经典理论**:**Coffman 条件**(死锁 4 个必要条件:互斥、持有且等待、不可剥夺、循环等待)由 E.G. Coffman 于 1971 年提出——4 个条件**必须同时满足**才发生死锁,因此**只要打破任意一个**就能避免死锁 - **固定锁顺序的局限**:固定锁顺序能避免死锁,但要求所有线程**对锁的顺序达成一致**(如统一 lockA → lockB);如果代码中有多处加锁点、顺序没有全局统一,依然可能死锁——大型系统中"锁排序"需要全局设计 - **tryLock 与死锁的关系**:第 7 节 LockDemo 学的 `tryLock()` / `tryLock(timeout)` 正是**避免死锁的重要手段**——拿不到锁就放弃(或超时放弃),并释放已持有的锁,从而打破"持有且等待",这是 ReentrantLock 比 synchronized 的明显优势之一(synchronized 无法超时) - **jstack 排查死锁**:生产环境死锁的标准排查手段是 **`jstack`** 命令(JDK 自带)——它会在线程转储中输出 **`Found one Java-level deadlock`**,并列出死锁的线程与各自持有的锁/等待的锁,是定位死锁最直接的工具 - **ConcurrentHashMap 与死锁**:`ConcurrentHashMap`(JDK5 引入的线程安全 Map)内部通过**分段锁 / CAS + synchronized 细粒度锁**实现线程安全,外部调用者**不需要手动加多把锁**,从源头避免了"同步嵌套导致的死锁"——这正是 Demo03 提到的"使用高级并发工具避免死锁" - **其他并发工具**:避免死锁还可使用 **`Semaphore`(信号量,限制资源访问数)**、**`CountDownLatch`(倒计时器,等待多线程完成)**、**`CyclicBarrier`(循环屏障)** 等 AQS 体系并发工具,减少手动锁管理 - **线程池的概念**:线程池是**存储线程的容器**——提前创建一批线程并常驻,任务来了**复用空闲线程**(而不是每次 new Thread().start());对应 Demo04 的"水池类比":线程池 = 水池(容器),线程 = 水(资源) - **JDK 提供的线程池**:JDK5 起提供 **`Executors` 工厂类**(`newFixedThreadPool` / `newCachedThreadPool` / `newSingleThreadExecutor` / `newScheduledThreadPool`)和底层实现 **`ThreadPoolExecutor`**——生产环境更推荐直接 new ThreadPoolExecutor(参数可控、避免 OOM) - **ThreadPoolExecutor 构造参数详解**:`corePoolSize`(核心线程常驻)、`maximumPoolSize`(最大线程 = 核心 + 临时)、`keepAliveTime` + `unit`(临时线程空闲回收时间)、`workQueue`(有界/无界队列,如 ArrayBlockingQueue / LinkedBlockingQueue / SynchronousQueue)、`threadFactory`(自定义线程名、守护属性)、`handler`(拒绝策略) - **拒绝策略(handler)四种**:`AbortPolicy`(默认,抛 RejectedExecutionException)/ `CallerRunsPolicy`(调用者线程自己执行任务)/ `DiscardPolicy`(直接丢弃,静默)/ `DiscardOldestPolicy`(丢弃队列最旧任务再尝试提交) - **execute 与 submit**:`pool.execute(Runnable)` 提交无返回值的任务;`pool.submit(Callable/Runnable)` 提交**带返回值**的任务并返回 `Future`(回顾 08-08 Callable + FutureTask——`submit` 返回的 Future 就是线程池版的 FutureTask) - **线程池与线程安全三特性**:线程池本身是**线程安全的**(内部队列、worker 管理都有锁保证);但池中多个线程处理共享数据时,**依然需要 volatile / synchronized / Lock 保证三大特性**——线程池解决"线程创建开销"问题,不解决"数据竞争"问题 - **线程池的正确关闭**:`shutdown()`(不再接受新任务,已提交任务执行完)与 `shutdownNow()`(立即中断所有任务);关闭后可用 `awaitTermination(timeout, unit)` 等待线程池完全终止——不关闭线程池程序可能一直不退出(池中非守护线程活着) - **线程池参数调优**:CPU 密集型任务建议 `corePoolSize = CPU 核数 + 1`;IO 密集型任务建议 `corePoolSize = CPU 核数 × 2`(或更大)——因为 IO 任务大多在等待,可容纳更多线程;队列大小根据任务峰值设计,避免 OOM