# 2026-08-11 volatile 与 Lock 锁练习(课堂练习用) > **说明**:练习今日新学知识点——**volatile 关键字**(解决可见性 / 有序性、不能保证原子性、JVM 内存模型:工作内存 CPU 缓存 vs 主内存、可见性问题"程序永不退出"、volatile 三大语义)、**线程安全三大特性解决方案**(原子性→synchronized/Lock/Atomic、可见性→volatile/synchronized/Lock、有序性→volatile/synchronized)、**计数器问题升级**(volatile 修饰 count 不能保证原子性、synchronized 保证原子性、1000 线程验证)、**Lock 锁(ReentrantLock 可重入锁)**(lock() / unlock() 基本用法、unlock 必须放 finally 铁律、tryLock() / tryLock(timeout) / lockInterruptibly() 避免死锁、公平锁 / 非公平锁、synchronized vs Lock 七维对比)。 --- ## 练习一:volatile 可见性 —— "程序永不退出"复现与修复 **难度**:⭐⭐⭐ **知识点**:`volatile` 可见性、JVM 内存模型(工作内存 CPU 缓存 vs 主内存)、`while(running)` 空循环、方法引用 `对象::方法` 创建线程、`Thread.sleep()` **场景描述**:课堂学了 **volatile 的可见性语义**:每个线程有独立的工作内存(CPU 缓存),读变量优先从工作内存读,写变量先写工作内存再刷主存(有延迟)。不加 volatile 时,main 线程把 `running` 改成 false 写入了主内存,但 worker 线程一直读自己缓存里的旧值 true,导致 `while(running)` 永不退出;加了 `volatile` 后,读 volatile 变量强制从主存读,worker 能立刻看到修改正常退出。 ### 案例需求 复现并修复"程序永不退出":worker 线程 `while(running)` 空循环,main 线程 sleep 后调用 `stop()` 修改 running,验证加/不加 volatile 的差异。 ### 实现步骤 1. 定义一个类,成员变量 `private volatile boolean running = true;` 2. `work()` 方法:打印"开始",进入 `while(running)` 空循环(什么都不做),退出后打印"线程退出" 3. `stop()` 方法:把 `running` 改为 false 4. main 中用方法引用 `new Thread(vd::work, "worker")` 创建并启动线程 5. main 线程 `sleep(1000)` 让 worker 先跑起来,再调用 `vd.stop()` 6. 观察运行结果:**先去掉 volatile 复现永不退出,再加 volatile 修复** ### 代码框架 ```java public class VolatileVisibilityTest { //如果不加volatile,其他线程可能永远都看不到修改 private volatile boolean running = true; // TODO 1: work() 方法——打印"work 线程开始......",while(running) 空循环,退出后打印"线程退出......" // TODO 2: stop() 方法——把 running 改为 false public static void main(String[] args) throws InterruptedException { VolatileVisibilityTest vd = new VolatileVisibilityTest(); // TODO 3: 用方法引用创建线程:new Thread(vd::work, "worker"),start() 启动 // 等价于 new Thread(() -> vd.work(), "worker") // 等价于 new Thread(new Runnable(){ public void run(){ vd.work(); } }, "worker") // TODO 4: 主线程 Thread.sleep(1000) 让 worker 先跑起来 // TODO 5: 主线程调用 vd.stop() 修改 running } } ``` **输出示例**: ``` work 线程开始...... ← 若无 volatile:永不打印"线程退出......"(程序卡死) work 线程退出...... ← 加了 volatile:能正常退出 ``` **选做加分**: - 把 `running` 的 volatile 去掉再运行,观察 worker 线程是否**永远打印不出"线程退出......"**(程序卡死) - 在 while 循环里加上 `System.out.println("work....")` 再运行,观察输出是否出现——体会空循环 vs 带输出对缓存可见性的影响 - 把 `vd::work` 改成 Lambda `() -> vd.work()` 和匿名内部类两种写法,对比三种创建线程的等价性 **思考题**: 1. 为什么 main 线程修改了 `running`,worker 线程却看不到?(提示:每个线程有自己的工作内存 CPU 缓存,worker 一直读缓存里的旧值 true,刷新到主内存有延迟) 2. `volatile` 解决了什么问题?(提示:可见性——写 volatile 变量立即刷主存、读 volatile 变量强制从主存读) 3. `volatile` 能保证原子性吗?(提示:不能!`volatile count++` 依旧会丢失更新) --- ## 练习二:计数器问题升级 —— volatile 不保证原子性 vs synchronized 保证原子性 **难度**:⭐⭐⭐⭐ **知识点**:`volatile` 不保证原子性、`synchronized` 保证原子性、`count++` 读-改-写 3 步、线程数组 + `join()` 等待、Lambda 创建 Runnable **场景描述**:课堂学了**计数器问题升级**:上节课(08-10)Counter 用普通 Integer count,1000 线程并发 count++ 结果小于 1000;这节课给 count 加了 **volatile** 修饰,但**结果照样小于 1000**——证明 volatile 只能保证可见性、**不能保证原子性**(count++ 的"读-改-写"3 步依旧可能被打断)。只有加 **synchronized** 的 increment() 才能保证原子性,count **必然是 1000**。 ### 案例需求 创建 Counter 类:`volatile Integer count` + `synchronized increment()`,用 1000 线程并发验证:加 synchronized 后 count 必然 1000。 ### 实现步骤 1. Counter 类:`private volatile Integer count = 0;` 2. `increment()` 方法:`count++`(注释标明底层是读-改-写 3 步) 3. 给 increment() 加 `synchronized` 修饰(同步实例方法,锁 this) 4. main:创建 Counter,1000 个线程并发调用 increment() 5. 用线程数组 + `join()` 等待所有线程结束 6. 打印最终 count,验证必然是 1000 ### 代码框架 ```java public class CounterAtomicityTest { //使用volatile修饰也不能保证原子性 private volatile Integer count = 0; //只要加了synchronized,就能够保证原子性 // TODO 1: 给 increment() 加 synchronized 修饰 public 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 { CounterAtomicityTest counter = new CounterAtomicityTest(); int threadCount = 1000; // TODO 2: 创建线程数组 Thread[threadCount],每个线程执行 counter.increment(),start() Thread[] threads = new Thread[threadCount]; for (int i = 0; i < threadCount; i++) { threads[i] = new Thread(() -> { counter.increment(); //执行相加 }); threads[i].start(); } // TODO 3: 等待所有线程结束 for (Thread t : threads) t.join(); //理论上应该是1000,加上synchronized后必然是1000 System.out.println("最终 count=" + counter.getCount()); } } ``` **输出示例**: ``` 最终 count=1000 ← 加了 synchronized,必然是 1000 ``` **选做加分**: - 把 increment() 的 `synchronized` 去掉(保留 count 的 volatile),运行观察 count **依然小于 1000**——直接验证"volatile 不能保证原子性,必须 synchronized" - 把 threadCount 改成 10000 再运行,观察线程多时竞争更激烈、结果偏差更明显 - 改用 `AtomicInteger` 代替 volatile Integer + synchronized,观察 `incrementAndGet()` 是否无需加锁也能保证原子性(展望拓展知识) **思考题**: 1. `count++` 表面是一行代码,底层被拆成几步?(提示:3 步——①读当前 count 值 ②count+1 ③赋值给 count,即"读-改-写") 2. 为什么 volatile 修饰了 count 还是小于 1000?(提示:volatile 只保证可见性,不保证原子性——"读-改-写"3 步依旧可能被打断,多个线程读旧值计算后互相覆盖) 3. `synchronized` 为什么能保证原子性?(提示:同步实例方法锁 this,同一时刻只有一个线程能执行 count++,其他线程排队等待) 4. `join()` 在这里起什么作用?(提示:让 main 等待 1000 个线程全部结束后再打印 count,否则可能打印到还没执行完的中间值) --- ## 练习三:Lock 锁基本用法 —— ReentrantLock 计数器(lock + unlock + finally) **难度**:⭐⭐⭐⭐ **知识点**:`Lock` 接口、`ReentrantLock` 可重入锁、`lock()` / `unlock()`、unlock 必须放 **finally** 铁律、公平锁 / 非公平锁、10000 线程 + `join()` 验证 **场景描述**:课堂学了 **Lock 锁(ReentrantLock 可重入锁)**:JDK1.5 提供 Lock 接口及实现类 ReentrantLock。使用 Lock 有一条铁律——**synchronized 是自动释放锁的,而 Lock 必须手动释放锁,且 `unlock()` 必须放到 finally 块中**,否则代码抛异常或提前 return 时锁永远不会释放,其他线程会永远阻塞(死锁)。本例用 ReentrantLock 实现一个计数器,验证 10000 线程并发后 count 正确。 ### 案例需求 创建 LockCounter 类:`ReentrantLock lock` + `increment()`(lock → try → count++ → finally unlock),用 10000 线程并发验证 count 必然正确。 ### 实现步骤 1. 定义 `ReentrantLock lock = new ReentrantLock();`(默认非公平锁;`new ReentrantLock(true)` 是公平锁) 2. `increment()` 方法:`lock.lock()` 加锁 → `try { count++; } finally { lock.unlock(); }` 3. main:创建 LockCounter,10000 个线程并发调用 increment() 4. 用线程数组 + `join()` 等待所有线程结束 5. 打印最终 count,验证正确性 ### 代码框架 ```java import java.util.concurrent.locks.ReentrantLock; public class LockCounterTest { private Integer count = 0; //创建锁对象---默认是非公平锁 // TODO 1: ReentrantLock lock = new ReentrantLock(); // 公平锁写法:new ReentrantLock(true); 按照申请顺序获取锁,性能略低 //---------基本用法:lock()+unlock() ,前提是必须要finally public void increment() { // TODO 2: lock.lock(); 加锁 // TODO 3: try { count++; } finally { lock.unlock(); } 释放锁必须放finally } public Integer getCount() { return count; } public static void main(String[] args) throws InterruptedException { LockCounterTest lc = new LockCounterTest(); // TODO 4: 创建线程数组 Thread[10000],每个线程执行 lc.increment(),start() Thread[] threads = new Thread[10000]; for (int i = 0; i < threads.length; i++) { threads[i] = new Thread(lc::increment); threads[i].start(); } // TODO 5: 等待所有线程结束 for (Thread t : threads) t.join(); System.out.println("最终 count=" + lc.getCount()); } } ``` **输出示例**: ``` 最终 count=10000 ← 加了 Lock 锁,count 必然正确 ``` **选做加分**: - 把 `finally { lock.unlock(); }` 去掉(直接 count++ 后 unlock),模拟"提前 return 时锁不释放"的死锁场景 - 在 increment() 里加 `if (count == 500) return;` 再运行,观察不加 finally 时是否死锁——体会铁律 - 分别用 `new ReentrantLock()`(非公平)和 `new ReentrantLock(true)`(公平)创建锁,观察竞争线程获取锁的顺序差异 **思考题**: 1. 为什么 `unlock()` 必须放到 finally 块中?(提示:如果代码抛异常或提前 return,锁永远不会释放,其他线程永久阻塞——死锁) 2. 非公平锁和公平锁的区别是什么?(提示:非公平锁释放后所有线程竞争(性能高);公平锁按申请顺序获取锁(性能略低)) 3. Lock 相比 synchronized 需要手动释放锁,这是缺点吗?(提示:是使用成本更高的体现,但也因此支持 tryLock / lockInterruptibly 等精细控制) 4. `lc::increment` 是什么写法?(提示:方法引用,等价于 `() -> lc.increment()`) --- ## 练习四:Lock 锁进阶 —— tryLock / tryLock(timeout) / lockInterruptibly 避免死锁 **难度**:⭐⭐⭐⭐⭐ **知识点**:`tryLock()` 尝试获取锁、`tryLock(timeout, TimeUnit)` 限时等待、`lockInterruptibly()` 可中断获取锁、避免死锁、`InterruptedException`、catch 后重设中断标志 **场景描述**:课堂学了 Lock 锁的**进阶用法**:基本用法 `lock()` 如果拿不到锁会**无限期等待**(可能死锁);而 `tryLock()` **获取不到就算了**(避免死锁的手段)、`tryLock(timeout)` **最多等指定时间**、`lockInterruptibly()` **等待中可被 interrupt() 中断**。本例实现三种进阶方法,体会 Lock 比 synchronized 更精细的锁控制。 ### 案例需求 实现一个 LockDemo 类,包含三种进阶加锁方法(tryLock / tryLock(timeout) / lockInterruptibly),并演示如何避免死锁、响应中断。 ### 实现步骤 1. `tryIncrement()`:`if (lock.tryLock())` 获取到就 count++ 并返回 true,没获取到返回 false 2. `timedIncrement()`:`lock.tryLock(1, TimeUnit.SECONDS)` 最多等 1 秒,超时返回 false;处理 InterruptedException 3. `interruptLock()`:`lock.lockInterruptibly()` 等待中可被 interrupt 打断;catch 中打印提示并重设中断标志 4. main:创建 LockDemo,10000 线程并发调用 interruptLock(),join 等待后打印 count ### 代码框架 ```java import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; public class LockAdvanceTest { private Integer count = 0; private final ReentrantLock lock = new ReentrantLock(); //--------进阶用法:tryLock()尝试获取锁,如果实在拿不到就算了,继续干别的。避免死锁的手段。 public boolean tryIncrement() { // TODO 1: if (lock.tryLock()) { //立刻尝试获取锁 // try { count++; return true; } // finally { lock.unlock(); } // } // TODO 2: return false; //没抢到锁就返回false return false; } //--------进阶用法:tryLock(timeout)限时等待 public boolean timedIncrement() { // TODO 3: try { // if (lock.tryLock(1, TimeUnit.SECONDS)) { //最多等待1秒 // try { count++; return true; } // finally { lock.unlock(); } // } // } catch (InterruptedException e) { throw new RuntimeException(e); } // TODO 4: return false; return false; } //--------进阶用法:可中断的获取锁(和sleep中断机制配合) public void interruptLock() { // TODO 5: 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 { LockAdvanceTest ld = new LockAdvanceTest(); // TODO 6: 创建线程数组 Thread[10000],每个线程执行 ld::interruptLock,start() Thread[] threads = new Thread[10000]; for (int i = 0; i < threads.length; i++) { threads[i] = new Thread(ld::interruptLock); threads[i].start(); } // TODO 7: 等待所有线程结束 for (Thread t : threads) t.join(); System.out.println("最终 count=" + ld.getCount()); } } ``` **输出示例**: ``` 最终 count=10000 ← 用 lockInterruptibly + finally unlock 保证原子性 (若某线程在等待锁时被中断,会打印:等待锁的过程中被中断) ``` **选做加分**: - 写两个线程:一个持有锁不放(sleep 5 秒),另一个分别用 `tryLock()` / `tryLock(1,SECONDS)` / `lockInterruptibly()` 尝试拿锁,观察三种方法在"拿不到锁"时的不同表现(立即返回 false / 等 1 秒返回 false / 等待中被 interrupt 中断) - 在 `tryIncrement()` 外打印返回值,观察多个线程竞争锁时,拿不到锁的线程返回 false、继续执行而不阻塞——体会"避免死锁" - 在 main 中给一个正在等待 lockInterruptibly 的线程调用 `interrupt()`,观察 catch 中打印"等待锁的过程中被中断" **思考题**: 1. `lock()` 和 `tryLock()` 的区别是什么?(提示:lock() 拿不到锁就无限期等待;tryLock() 立刻尝试,拿不到立即返回 false——tryLock 是避免死锁的手段) 2. `tryLock(timeout)` 的 timeout 参数是什么意思?(提示:最多等待指定时间,超时还没拿到锁就返回 false——拿不到锁不会再傻等) 3. `lockInterruptibly()` 和 `tryLock()` 都能避免死锁吗?(提示:都能!lockInterruptibly 是"继续等但可被中断";tryLock 是"拿不到就算了"——都是 Lock 比 synchronized 更精细的控制) 4. 为什么 catch 里要 `Thread.currentThread().interrupt()` 重设中断标志?(提示:InterruptedException 抛出时会清除中断标志,重设后让上层 isInterrupted() 能感知到中断——回顾 08-10 中断机制) --- ## 练习五:综合应用 —— 用 Lock 锁改造卖票问题(synchronized vs Lock 对比) **难度**:⭐⭐⭐⭐⭐ **知识点**:`synchronized` vs `Lock` 七维对比、Lock 锁解决数据竞争、卖票问题(相同票 / 负数票)、三种线程安全方案(synchronized / Lock / 原子类)、多线程共享同一对象 **场景描述**:课堂学了 **synchronized vs Lock 的七维对比**(语法简洁度 / 锁释放 / 是否可中断 / 是否可超时等待 / 是否支持锁读 / 底层实现 Monitor Lock vs AQS / JDK21 版本更新性能相近)。08-10 的课后作业用 **synchronized** 修复卖票问题(Demo_03);本例要求用 **Lock 锁(ReentrantLock)** 改造同一个卖票问题,对比两种锁的写法差异,体会"解决同一个线程安全问题,synchronized 和 Lock 各有写法"。 ### 案例需求 把 homework0810 Demo_03 的卖票类(SellTicket)中的 `synchronized(this)` 改造成 ReentrantLock 加锁(lock + try + finally unlock),3 个窗口线程卖 100 张票,验证不再出现相同票 / 负数票。 ### 实现步骤 1. SellTicket 实现 Runnable,`private int tickets = 100;` 2. 创建 `ReentrantLock lock`(锁对象) 3. `sell()` 卖票逻辑:if (tickets > 0) { sleep(10); 打印出售信息; tickets--; } 4. run() 中:`lock.lock()` → try { sell(); } finally { lock.unlock(); } 5. main:创建同一个 SellTicket 对象,3 个窗口线程(窗口1/2/3)启动卖票 6. 用线程数组 + `join()` 等待所有窗口卖票结束 ### 代码框架 ```java import java.util.concurrent.locks.ReentrantLock; public class LockSellTicketTest { static class SellTicket implements Runnable { private int tickets = 100; // 一共 100 张票 // TODO 1: 创建 ReentrantLock lock = new ReentrantLock(); public void sell() { 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) { // TODO 2: lock.lock(); → try { sell(); } finally { lock.unlock(); } // 对比 08-10 作业的 synchronized(this) { sell(); } } } } public static void main(String[] args) throws InterruptedException { SellTicket st = new SellTicket(); // TODO 3: 创建三个线程对象,模拟3个售票窗口(共用同一个st) // new Thread(st, "窗口1") / "窗口2" / "窗口3",start() 启动 // TODO 4: 用线程数组 + join() 等待所有线程卖票结束 } } ``` **输出示例**: ``` 窗口1正在出售第 100 张票 窗口3正在出售第 99 张票 窗口2正在出售第 98 张票 ... 窗口3正在出售第 1 张票 ← 每张票只出现一次,没有相同票 / 负数票 ``` **选做加分**: - 把 `lock.lock()` 换成 `lock.tryLock()` 或 `lock.tryLock(1, TimeUnit.SECONDS)` 再运行,观察卖票结果是否仍然正确(可能某窗口没抢到票直接跳过本轮) - 对比总结:synchronized 写法(`synchronized(this) { sell(); }`)和 Lock 写法(lock + try + finally unlock)的**七维差异**,写一篇对比小结 - 给 SellTicket 的卖票逻辑不用锁,多运行几次复现**相同票 / 负数票** bug,再加 Lock 锁修复——完整走一遍"复现 bug → Lock 修复"流程 **思考题**: 1. 为什么 3 个窗口必须**共用同一个** SellTicket 对象?(提示:只有共享同一个对象,lock 锁对象才一致,synchronized/Lock 才有效;若各自 new 对象,锁失效) 2. Lock 改造卖票和 synchronized 改造卖票,哪个更简单?(提示:synchronized 语法更简单(只需关键字);Lock 需要 try-finally 块手动释放——这正对应七维对比中的"语法简洁度") 3. 卖票问题为什么要锁住"判断-打印-减票"整体?(提示:这是读-改-写复合操作,必须整体原子执行,否则多个线程同时通过 if 判断导致相同票 / 负数票) 4. 结合七维对比,什么场景适合用 Lock?(提示:需要超时等待、响应中断、公平锁、读写分离等精细控制时;简单场景用 synchronized 更合适) --- # 参考答案要点 ## 练习一:VolatileVisibilityTest(volatile 可见性) ```java // 1、work() 方法(TODO 1) public void work() { System.out.println("work 线程开始......"); while (running) { //空循环,什么都不做.... } System.out.println("线程退出......"); } // 2、stop() 方法(TODO 2) public void stop() { running = false; } // 3、main 方法(TODO 3~5) public static void main(String[] args) throws InterruptedException { VolatileVisibilityTest vd = new VolatileVisibilityTest(); Thread t = new Thread(vd::work, "worker"); // 方法引用创建线程 t.start(); Thread.sleep(1000); vd.stop(); // 不加 volatile → 程序永不退出;加了 volatile → 正常退出 } ``` ## 练习二:CounterAtomicityTest(volatile vs synchronized) ```java // 1、给 increment() 加 synchronized(TODO 1) public synchronized void increment() { count++; // 读-改-写 3 步 } // 2、1000 线程并发 + join 等待(TODO 2~3) 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(); } System.out.println("最终 count=" + counter.getCount()); // 必然是 1000 ``` ## 练习三:LockCounterTest(ReentrantLock 基本用法) ```java // 1、创建锁对象(TODO 1) ReentrantLock lock = new ReentrantLock(); // 默认非公平锁 // 2、increment():lock + try + finally unlock(TODO 2~3) public void increment() { lock.lock(); // 加锁 try { count++; } finally { lock.unlock(); // 释放锁,必须要释放,否则其他线程永远拿不到锁 } } // 3、10000 线程 + join 等待(TODO 4~5) Thread[] threads = new Thread[10000]; for (int i = 0; i < threads.length; i++) { threads[i] = new Thread(lc::increment); threads[i].start(); } for (Thread t : threads) { t.join(); } System.out.println("最终 count=" + lc.getCount()); // 必然是 10000 ``` ## 练习四:LockAdvanceTest(tryLock / tryLock(timeout) / lockInterruptibly) ```java // 1、tryLock() 尝试获取锁(TODO 1~2) public boolean tryIncrement() { if (lock.tryLock()) { // 立刻尝试,获取到了就返回true,否则返回false try { count++; return true; } finally { lock.unlock(); } } return false; // 没抢到锁就返回false,继续干别的——避免死锁 } // 2、tryLock(timeout) 限时等待(TODO 3~4) 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; } // 3、lockInterruptibly() 可中断获取锁(TODO 5) public void interruptLock() { try { lock.lockInterruptibly(); // 等待锁的过程中可以被 interrupt() 打断 try { count++; } finally { lock.unlock(); } } catch (InterruptedException e) { System.out.println("等待锁的过程中被中断"); Thread.currentThread().interrupt(); // 重设中断标志 } } // 4、main:10000 线程执行 interruptLock + join(TODO 6~7) 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(); } System.out.println("最终 count=" + ld.getCount()); // 必然是 10000 ``` ## 练习五:LockSellTicketTest(Lock 改造卖票问题) ```java // 1、创建锁对象(TODO 1) private final ReentrantLock lock = new ReentrantLock(); // 2、run():lock + try + finally unlock 锁住卖票过程(TODO 2) @Override public void run() { while (tickets > 0) { lock.lock(); try { sell(); } finally { lock.unlock(); } } } // 3、3 个窗口线程共用同一个 st + join 等待(TODO 3~4) SellTicket st = new SellTicket(); Thread t1 = new Thread(st, "窗口1"); Thread t2 = new Thread(st, "窗口2"); Thread t3 = new Thread(st, "窗口3"); t1.start(); t2.start(); t3.start(); // join 等待(用线程数组写法) Thread[] windows = {t1, t2, t3}; for (Thread t : windows) { t.join(); } ``` --- # 涵盖知识点总览 | 知识点 | 说明 | |--------|------| | `volatile` 可见性 | 写 volatile 变量立即刷主存、读 volatile 变量强制从主存读,禁止使用 CPU 缓存 | | JVM 内存模型 | 每个线程有独立工作内存(CPU 缓存);读变量优先从工作内存读、刷新到主内存有延迟 | | 可见性问题"程序永不退出" | worker 线程 while(running) 空循环,读不到 main 线程的修改 → 永不退出;volatile 修复 | | volatile 三大语义 | ①可见性 ②禁止重排序(有序性)③不保证原子性 | | 方法引用创建线程 | `new Thread(vd::work, "worker")` 等价于 Lambda、等价于匿名 Runnable | | `volatile` 不保证原子性 | count++ 的"读-改-写"3 步依旧可能被打断,volatile 修饰 count 也小于理论值 | | `synchronized` 保证原子性 | 同步实例方法锁 this,同一时刻只有一个线程执行 count++ → count 必然是 1000 | | `count++` 非原子操作 | 底层 3 步:读 → 改 → 写;多线程并发时读到旧值覆盖新值 | | 线程数组 + `join()` | 1000/10000 线程并发后,for 循环 t.join() 等待所有线程结束再打印结果 | | `Lock` 接口 / `ReentrantLock` | JDK1.5 提供的显式锁机制;可重入锁,比 synchronized 更精细 | | `lock()` / `unlock()` | Lock 基本用法;unlock **必须放 finally**,否则死锁 | | unlock 放 finally 铁律 | 代码抛异常或提前 return 时锁不释放 → 其他线程永久阻塞(死锁) | | `tryLock()` | 立刻尝试获取锁,拿到返回 true,没拿到返回 false——**避免死锁的手段** | | `tryLock(timeout, unit)` | 最多等待指定时间,超时返回 false——拿不到锁不会再傻等 | | `lockInterruptibly()` | 等待锁的过程中可被 interrupt() 打断;catch 后重设中断标志 | | 公平锁 / 非公平锁 | `new ReentrantLock()` 非公平(释放后竞争);`new ReentrantLock(true)` 公平(按申请顺序,性能略低) | | synchronized vs Lock 七维对比 | 语法简洁度 / 锁释放 / 可中断 / 超时等待 / 锁读(读写分离)/ 底层实现(Monitor Lock vs AQS)/ JDK21 版本更新(性能相近) | | Lock 解决数据竞争 | 卖票问题用 lock + try + finally unlock 锁住"判断-打印-减票"整体,消除相同票 / 负数票 | | 三种线程安全方案 | synchronized(关键字简单可靠)/ Lock(精细控制)/ 原子类(AtomicInteger 无锁 CAS 性能最优) |