|
|
@@ -0,0 +1,611 @@
|
|
|
+# 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 性能最优) |
|
|
|
+
|
|
|
+
|
|
|
+
|