2026年8月11日-作业.md 20 KB

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 线程计数器结果。

题目要求:

/**
 * @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 的铁律。

题目要求:

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() 观察线程池状态,并注释分析每个任务走了执行流程的哪一步(核心线程 / 工作队列 / 非核心线程 / 拒绝策略)。

题目要求:

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 自带,无需额外依赖)