20260811-volatile与Lock锁练习.md 28 KB

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 修复

代码框架

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

代码框架

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,验证正确性

代码框架

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

代码框架

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() 等待所有窗口卖票结束

代码框架

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 可见性)

// 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)

// 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 基本用法)

// 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)

// 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 改造卖票问题)

// 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 性能最优)