# 2026年8月11日 课后作业 — volatile 关键字、Lock 锁(可重入锁)与线程池 > **说明**:本次课程学习了 **volatile 关键字**(解决可见性 / 有序性、不能保证原子性、JVM 内存模型:工作内存 CPU 缓存 vs 主内存、可见性问题"程序永不退出"、volatile 三大语义:可见性 / 禁止重排序 / 不保证原子性)、**Lock 锁(ReentrantLock 可重入锁)**(相比 synchronized 的优势、unlock 必须放 finally 铁律、公平锁 / 非公平锁、tryLock / tryLock(timeout) / lockInterruptibly 避免死锁、synchronized vs Lock 七维对比)以及 **线程池**(ThreadPoolExecutor 的 7 个参数、执行流程:核心线程 → 工作队列 → 非核心线程 → 拒绝策略、线程复用 + 任务排队 + 动态扩容 + 上限保护、饭店餐馆类比、execute / shutdown)。 > > 本作业共 **3 道题**:**基础题(必做)**——volatile 可见性"程序永不退出"复现与修复 + 计数器原子性对比;**进阶题(必做)**——ReentrantLock 可重入锁计数器(四种加锁用法);**挑战题(选做)**——线程池 ThreadPoolExecutor 任务提交与拒绝策略观察。覆盖今日核心知识点。 --- ## 📌 作业总体要求 ### 一、命名要求 | 项目 | 要求 | |------|------| | **类名** | `Demo_题号_你的姓名`(如 `Demo_01_张三`、`Demo_02_李四`、`Demo_03_王五`) | | **文件头注释** | 每个 Java 文件头部需包含 `@author`、`@title`、`@create` 文档注释 | ### 二、提交方式与截止时间 | 项目 | 要求 | |------|------| | **提交方式** | 将源码文件 + 运行结果截图提交至班级指定平台 | | **截止时间** | 下次上课前 | --- ## 一、基础题(必做) ### 题目 1:volatile 可见性与原子性 —— 程序永不退出 + 计数器对比 VolatileCounterDemo(⭐⭐ 必做) **知识点**:`volatile` 可见性(写 volatile 变量立即刷主存、读 volatile 变量强制从主存读)、JVM 内存模型(工作内存 CPU 缓存 vs 主内存、刷新有延迟)、可见性问题"程序永不退出"、`volatile` **不保证原子性**(count++ 读-改-写 3 步依旧会被打断)、`synchronized` 保证原子性、方法引用 `对象::方法` 创建线程、线程数组 + `join()` 等待 **场景描述**:课堂用 `VolatileDemo` 演示了可见性问题——worker 线程 `while(running)` 空循环,main 线程修改 `running` 后 worker 读不到(读自己缓存旧值)导致**程序永不退出**,加 `volatile` 后正常退出;用 `Counter` 演示了 **volatile 不保证原子性**(1000 线程并发 count++ 结果小于 1000),只有加 `synchronized` 才保证原子性。请综合实现:**① 可见性**——用 volatile 解决"程序永不退出";**② 原子性**——对比 volatile 修饰 count 与 synchronized increment 的 1000 线程计数器结果。 **题目要求**: ```java /** * @author 你的姓名 * @title 题目1:volatile 可见性 + 计数器原子性对比 * @create 2026/8/11 */ public class Demo_01_姓名 { // ========== ① 可见性部分:running 标志 ========== // TODO 1: private volatile boolean running = true; // 加 volatile 解决可见性 // TODO 2: work() 方法——打印"work 线程开始......",while(running) 空循环,退出后打印"线程退出......" // TODO 3: stop() 方法——把 running 改为 false // ========== ② 原子性部分:计数器 ========== // TODO 4: private volatile Integer count = 0; // 即使加 volatile,count++ 也不保证原子性 // TODO 5: public synchronized void increment() { count++; } // 加 synchronized 才保证原子性 // 注释标明 count++ 底层是 3 步:读 → 改 → 写 public static void main(String[] args) throws InterruptedException { // ========== ③ 可见性演示 ========== // TODO 6: 创建对象 vd,用方法引用 new Thread(vd::work, "worker") 启动 // TODO 7: 主线程 sleep(1000) 让 worker 先跑起来,再调用 vd.stop() // 观察:不加 volatile 永不打印"线程退出......";加 volatile 正常退出 // ========== ④ 原子性演示 ========== // TODO 8: 创建 Counter 对象,创建 1000 个线程并发执行 increment(),start() // TODO 9: 用 for (Thread t : threads) t.join() 等待所有线程结束 // TODO 10: 打印最终 count,验证加 synchronized 后必然是 1000 } } ``` **预期输出示例**(每次运行顺序可能不同): ``` work 线程开始...... work 线程退出...... ← 加了 volatile,worker 能看到 running 的修改,正常退出 最终 count=1000 ← 加了 synchronized,count 必然是 1000 ``` **要求说明**: 1. `running` 必须加 `volatile` 修饰,`work()` 用 `while(running)` 空循环(循环体不要打印,才能复现可见性问题) 2. 必须用方法引用 `new Thread(vd::work, "worker")` 创建 worker 线程(对比 Lambda / 匿名内部类三种写法) 3. `count` 必须加 `volatile` 修饰,`increment()` 加 `synchronized` 修饰(同步实例方法锁 this) 4. 必须用**线程数组 + `join()`** 等待 1000 个线程全部结束再打印 count 5. 在代码注释中写出你的结论:**volatile 解决什么问题?不能解决什么问题?** **💡 提示**: - **可见性问题根源**:每个线程有自己的工作内存(CPU 缓存),读变量优先从工作内存读、写变量先写工作内存再刷主存(**有延迟**)——worker 一直读缓存里的旧值 true - **volatile 三大语义**:①可见性(写立即刷主存、读强制从主存读)②禁止重排序(有序性)③**不保证原子性** - **count++ 非原子**:表面一行代码,底层 3 步(读 → 改 → 写);volatile 无法把 3 步变 1 步,多个线程读旧值计算互相覆盖 → 结果小于 1000 - **synchronized 保证原子性**:同一时刻只有一个线程能执行 count++,其他线程排队等待 → count 必然是 1000 **🎯 挑战加分(选做)**: 1. 把 `running` 的 volatile 去掉再运行,观察 worker 线程是否**永远打印不出"线程退出......"**(程序卡死) 2. 把 `increment()` 的 synchronized 去掉(保留 count 的 volatile)运行,观察 count **依然小于 1000**——直接验证"volatile 不能保证原子性" 3. 改用 `AtomicInteger` 代替 volatile + synchronized,观察 `incrementAndGet()` 是否无需加锁也能保证原子性(预习原子类) --- ## 二、进阶题(必做) ### 题目 2:ReentrantLock 可重入锁 —— 四种加锁用法计数器 LockCounterDemo(⭐⭐⭐ 必做) **知识点**:`Lock` 接口、**`ReentrantLock`(可重入锁)**、`lock()` / `unlock()` 基本用法、**unlock 必须放 finally 铁律**(否则死锁)、公平锁 / 非公平锁(`new ReentrantLock()` vs `new ReentrantLock(true)`)、`tryLock()`(获取不到就算了,避免死锁)、`tryLock(timeout, unit)`(限时等待)、`lockInterruptibly()`(可中断获取锁)、synchronized vs Lock 七维对比、方法引用 `ld::increment`、10000 线程 + `join()` 验证 **场景描述**:课堂用 `LockDemo` 演示了 ReentrantLock 的**四种用法**——①基本用法 `lock() + try-finally unlock` ②`tryLock()` 尝试获取(拿不到返回 false)③`tryLock(timeout)` 限时等待 ④`lockInterruptibly()` 可中断获取。请实现一个计数器类:**用 ReentrantLock 的四种加锁方式分别写 4 个方法**,并用 10000 线程验证加锁后 count 正确,体会 `unlock()` 必须放 finally 的铁律。 **题目要求**: ```java import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; /** * @author 你的姓名 * @title 题目2:ReentrantLock 可重入锁四种用法计数器 * @create 2026/8/11 */ public class Demo_02_姓名 { private Integer count = 0; // TODO 1: ReentrantLock lock = new ReentrantLock(); // 默认非公平锁 // 公平锁写法:new ReentrantLock(true); // 按申请顺序获取锁,性能略低 // ========== ① 基本用法:lock() + unlock(),unlock 必须放 finally ========== public void increment() { // TODO 2: lock.lock(); 加锁 // TODO 3: try { count++; } finally { lock.unlock(); } 释放锁必须放finally,否则死锁 } // ========== ② tryLock():获取不到就算了,避免死锁 ========== public boolean tryIncrement() { // TODO 4: if (lock.tryLock()) { 立刻尝试,拿到返回true,否则false // try { count++; return true; } // finally { lock.unlock(); } // } // TODO 5: return false; 没抢到锁就返回false,继续干别的 return false; } // ========== ③ tryLock(timeout):限时等待 ========== public boolean timedIncrement() { // TODO 6: try { // if (lock.tryLock(1, TimeUnit.SECONDS)) { // 最多等待1秒 // try { count++; return true; } // finally { lock.unlock(); } // } // } catch (InterruptedException e) { throw new RuntimeException(e); } // TODO 7: return false; return false; } // ========== ④ lockInterruptibly():可中断获取锁 ========== public void interruptLock() { // TODO 8: try { // lock.lockInterruptibly(); // 等待锁的过程中可被 interrupt() 打断 // try { count++; } // finally { lock.unlock(); } // } catch (InterruptedException e) { // System.out.println("等待锁的过程中被中断"); // Thread.currentThread().interrupt(); // 重设中断标志 // } } public Integer getCount() { return count; } public static void main(String[] args) throws InterruptedException { // ========== ⑤ 验证:10000 线程并发 increment() ========== // TODO 9: 创建 LockCounter 对象,创建线程数组 Thread[10000],每个线程执行 对象::increment,start() // TODO 10: for (Thread t : threads) t.join(); // 等待所有线程结束 // TODO 11: 打印最终 count,验证加了 Lock 锁后 count 必然是 10000 } } ``` **预期输出示例**: ``` 最终 count=10000 ← 用 lock + finally unlock 保护 count++,count 必然正确 ``` **要求说明**: 1. **四种加锁方法都必须实现**:`increment()`(lock/unlock)、`tryIncrement()`(tryLock)、`timedIncrement()`(tryLock(timeout))、`interruptLock()`(lockInterruptibly) 2. 每个拿到锁的方法都必须用 `try { ... } finally { lock.unlock(); }` 释放锁——**unlock 必须放 finally** 3. main 中必须用**线程数组 + `join()`** 等待 10000 个线程结束再打印 count 4. 必须用**方法引用** `对象::increment` 提交任务(对比 Lambda 写法) 5. 在代码注释中写出结论:**unlock 为什么要放 finally?如果 count++ 抛异常会发生什么?** **💡 提示**: - **unlock 放 finally 铁律**:synchronized 是自动释放锁的;Lock **必须手动释放**,`unlock()` **必须放 finally**——否则代码抛异常或提前 return 时锁永不释放,其他线程永久阻塞(**死锁**) - **tryLock() 避免死锁**:获取不到就返回 false,线程继续干别的,不阻塞等待(打破"持有且等待") - **tryLock(timeout)**:最多等指定时间,超时返回 false——"拿不到锁不会再傻等" - **lockInterruptibly()**:等待锁的过程中可被 `interrupt()` 打断;catch 后 `Thread.currentThread().interrupt()` 重设中断标志(回顾 08-10 中断机制) - **公平锁 vs 非公平锁**:`new ReentrantLock()` 非公平(释放后所有线程竞争,性能高);`new ReentrantLock(true)` 公平(按申请顺序,性能略低) - **七维对比**:synchronized vs Lock——语法简洁度 / 锁释放 / 可中断 / 超时等待 / 锁读(读写分离)/ 底层实现(Monitor Lock vs AQS)/ JDK21 版本更新(性能相近) **🎯 挑战加分(选做)**: 1. 把 `increment()` 的 `finally { lock.unlock(); }` 去掉(直接 count++ 后 unlock),模拟"提前 return 时锁不释放"的死锁场景,观察运行现象 2. 写两个线程:一个持有锁不放(sleep 5 秒),另一个分别用 `tryLock()` / `tryLock(1, SECONDS)` / `lockInterruptibly()` 尝试拿锁,观察三种方法在"拿不到锁"时的不同表现 3. 用 ReentrantLock 改造 08-10 的卖票问题(`lock.lock()` → try { sell(); } finally { lock.unlock(); }),对比 synchronized 写法的差异(回顾七维对比) --- ## 三、挑战题(选做) ### 题目 3:线程池 —— ThreadPoolExecutor 任务提交与执行流程观察 ThreadPoolTaskDemo(⭐⭐⭐⭐ 选做) **知识点**:**线程池(Thread Pool)**(存储线程的容器、水池类比)、new Thread().start() 的三个问题(创建/销毁开销大、线程数量不受控、资源利用率低)、解决方案【**线程复用 + 任务排队 + 动态扩容 + 上限保护**】、**ThreadPoolExecutor 的 7 个参数**(corePoolSize / maximumPoolSize / keepAliveTime / unit / workQueue / threadFactory / handler)、**执行流程**(核心线程 → 工作队列 → 非核心线程 → 拒绝策略)、**拒绝策略**(AbortPolicy 抛 RejectedExecutionException)、`execute()` 提交任务、`getActiveCount()` / `getQueue().size()` / `shutdown()`、饭店餐馆类比 **场景描述**:课堂用 `Demo04` 演示了创建线程池(`new ThreadPoolExecutor(3, 5, 60, SECONDS, ArrayBlockingQueue(10), defaultThreadFactory, AbortPolicy)`);用 `ThreadPoolDemo` 演示了 **8 个任务提交到 2 核心 4 最大 队列 2 的线程池**——2 个核心执行 + 2 个排队 + 2 个创建临时线程 + 2 个被拒绝(AbortPolicy)。请实现:**创建自己的线程池**(参数自选),提交 **8 个耗时任务**(每个 sleep 2 秒占住线程),用 `getActiveCount()` 和 `getQueue().size()` 观察线程池状态,并**注释分析每个任务走了执行流程的哪一步**(核心线程 / 工作队列 / 非核心线程 / 拒绝策略)。 **题目要求**: ```java import java.util.concurrent.*; /** * @author 你的姓名 * @title 题目3:线程池 ThreadPoolExecutor 任务提交与执行流程观察 * @create 2026/8/11 */ public class Demo_03_姓名 { public static void main(String[] args) { // ========== ① 创建线程池 ========== // TODO 1: new ThreadPoolExecutor( // 2, // corePoolSize-核心线程数 // 4, // maximumPoolSize-最大线程数(核心 + 临时) // 60, // keepAliveTime-非核心线程空闲存活时间 // TimeUnit.SECONDS, // unit-时间单位 // new ArrayBlockingQueue<>(2), // workQueue-工作队列 容量为2 // Executors.defaultThreadFactory(), // threadFactory-线程工厂 // new ThreadPoolExecutor.AbortPolicy() // handler-拒绝策略(默认抛异常) // ); // ========== ② 连续提交 8 个耗时任务 ========== // TODO 2: for (int i = 1; i <= 8; i++) { // int taskId = i; // try { // pool.execute(() -> { // 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()); // } // } // ========== ③ 观察线程池状态 ========== // TODO 3: 打印 pool.getActiveCount() // 活动线程数(核心 + 临时) // TODO 4: 打印 pool.getQueue().size() // 队列中排队任务数 // ========== ④ 关闭线程池 ========== // TODO 5: pool.shutdown(); // 关闭线程池:不再接受新任务,已提交任务继续执行完 } } ``` **预期输出示例**(每次运行顺序可能不同): ``` pool-1-thread-1执行任务1 ← 核心线程 1 pool-1-thread-2执行任务2 ← 核心线程 2 pool-1-thread-1执行任务3 ← 临时线程开始执行 pool-1-thread-2执行任务4 pool-1-thread-3执行任务5 ← 临时线程 pool-1-thread-4执行任务6 任务7被拒绝:... ← AbortPolicy 拒绝 任务8被拒绝:... 活动线程数:4 ← 2 核心 + 2 临时 队列中任务数:2 ← 排队等待的 2 个任务 ``` **要求说明**: 1. **7 个参数必须完整填写**,并在每个参数后加注释说明含义(corePoolSize / maximumPoolSize / keepAliveTime / unit / workQueue / threadFactory / handler) 2. 必须提交 **8 个任务**(每个 sleep 2 秒模拟耗时),用 try-catch 捕获 `RejectedExecutionException` 3. 必须打印 `getActiveCount()` 和 `getQueue().size()` 观察线程池状态 4. 必须调用 `pool.shutdown()` 关闭线程池 5. 在代码注释中**逐个分析 8 个任务各自走了执行流程的哪一步**(核心线程执行 / 工作队列排队 / 创建临时线程 / 拒绝策略拒绝),写出你的分析结论 **💡 提示**: - **线程池执行流程**:**核心线程 → 工作队列 → 非核心线程 → 拒绝策略**——线程不够了**不会第一时间创建新线程**,而是先把任务放工作队列;只有**队列满了**且线程数**还没到最大线程数**时才创建临时线程 - **8 任务走向分析**(2 核心 4 最大 队列 2):任务 1、2 → 核心线程执行;任务 3、4 → 工作队列排队;任务 5、6 → 队列满 → 创建临时线程;任务 7、8 → 线程满 + 队列满 → AbortPolicy 拒绝(抛 RejectedExecutionException) - **getActiveCount()**:当前活动线程数(这里 2 核心 + 2 临时 = 4);**getQueue().size()**:队列中排队任务数(这里 2 个) - **execute vs start**:`pool.execute(runnable)` 把任务交给线程池(**复用线程**,用 getName() 可看到多个任务共用同一线程名);`new Thread().start()` 每次创建新线程 - **shutdown()**:关闭线程池——不再接受新任务,已提交任务继续执行完;不 shutdown 程序可能一直不退出(池中非守护线程还活着) - **拒绝策略四种**:AbortPolicy(默认,抛异常)/ CallerRunsPolicy(调用者运行)/ DiscardPolicy(静默丢弃)/ DiscardOldestPolicy(丢弃最旧任务) **🎯 挑战加分(选做)**: 1. 修改参数:`corePoolSize` 改成 4、队列容量改成 4,重新提交 8 个任务,观察是否全部执行无人被拒(8 = 4 核心 + 4 排队)——体会 7 参数如何影响执行流程 2. 把 `AbortPolicy` 换成 `CallerRunsPolicy` 或 `DiscardPolicy`,提交超出容量的任务观察不同拒绝行为 3. 用 `pool.submit(() -> {...})` 提交**带返回值**的任务(返回 Future),用 `future.get()` 获取结果(回顾 08-08 Callable + FutureTask) 4. 把 `pool.shutdown()` 注释掉再运行,观察程序是否**一直不退出**——体会 shutdown 关闭线程池的作用 --- ## 四、提交要求 - **类名**:`Demo_题号_姓名`(如 `Demo_01_张三`、`Demo_02_李四`、`Demo_03_王五`) - **文件头注释**:每个 Java 文件头部必须包含 `@author`(你的姓名)、`@title`(题目名称)、`@create 2026/8/11` - **提交内容**:源码文件 + 运行结果截图 - **必做/选做**:基础题(题目 1)、进阶题(题目 2)为**必做**;挑战题(题目 3)为**选做**,鼓励学有余力的同学完成 - **运行环境**:JDK 8 及以上(题目 1 使用 `volatile` / `synchronized` 关键字,题目 2 使用 `java.util.concurrent.locks.ReentrantLock`,题目 3 使用 `java.util.concurrent.ThreadPoolExecutor`,均为 JDK 自带,无需额外依赖)