说明:练习今日新学知识点——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 可见性、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 的差异。
private volatile boolean running = true;work() 方法:打印"开始",进入 while(running) 空循环(什么都不做),退出后打印"线程退出"stop() 方法:把 running 改为 falsenew Thread(vd::work, "worker") 创建并启动线程sleep(1000) 让 worker 先跑起来,再调用 vd.stop()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 线程是否永远打印不出"线程退出......"(程序卡死)System.out.println("work....") 再运行,观察输出是否出现——体会空循环 vs 带输出对缓存可见性的影响vd::work 改成 Lambda () -> vd.work() 和匿名内部类两种写法,对比三种创建线程的等价性思考题:
running,worker 线程却看不到?(提示:每个线程有自己的工作内存 CPU 缓存,worker 一直读缓存里的旧值 true,刷新到主内存有延迟)volatile 解决了什么问题?(提示:可见性——写 volatile 变量立即刷主存、读 volatile 变量强制从主存读)volatile 能保证原子性吗?(提示:不能!volatile count++ 依旧会丢失更新)难度:⭐⭐⭐⭐
知识点: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。
private volatile Integer count = 0;increment() 方法:count++(注释标明底层是读-改-写 3 步)synchronized 修饰(同步实例方法,锁 this)join() 等待所有线程结束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
选做加分:
synchronized 去掉(保留 count 的 volatile),运行观察 count 依然小于 1000——直接验证"volatile 不能保证原子性,必须 synchronized"AtomicInteger 代替 volatile Integer + synchronized,观察 incrementAndGet() 是否无需加锁也能保证原子性(展望拓展知识)思考题:
count++ 表面是一行代码,底层被拆成几步?(提示:3 步——①读当前 count 值 ②count+1 ③赋值给 count,即"读-改-写")synchronized 为什么能保证原子性?(提示:同步实例方法锁 this,同一时刻只有一个线程能执行 count++,其他线程排队等待)join() 在这里起什么作用?(提示:让 main 等待 1000 个线程全部结束后再打印 count,否则可能打印到还没执行完的中间值)难度:⭐⭐⭐⭐
知识点: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 必然正确。
ReentrantLock lock = new ReentrantLock();(默认非公平锁;new ReentrantLock(true) 是公平锁)increment() 方法:lock.lock() 加锁 → try { count++; } finally { lock.unlock(); }join() 等待所有线程结束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 时锁不释放"的死锁场景if (count == 500) return; 再运行,观察不加 finally 时是否死锁——体会铁律new ReentrantLock()(非公平)和 new ReentrantLock(true)(公平)创建锁,观察竞争线程获取锁的顺序差异思考题:
unlock() 必须放到 finally 块中?(提示:如果代码抛异常或提前 return,锁永远不会释放,其他线程永久阻塞——死锁)lc::increment 是什么写法?(提示:方法引用,等价于 () -> lc.increment())难度:⭐⭐⭐⭐⭐
知识点:tryLock() 尝试获取锁、tryLock(timeout, TimeUnit) 限时等待、lockInterruptibly() 可中断获取锁、避免死锁、InterruptedException、catch 后重设中断标志
场景描述:课堂学了 Lock 锁的进阶用法:基本用法 lock() 如果拿不到锁会无限期等待(可能死锁);而 tryLock() 获取不到就算了(避免死锁的手段)、tryLock(timeout) 最多等指定时间、lockInterruptibly() 等待中可被 interrupt() 中断。本例实现三种进阶方法,体会 Lock 比 synchronized 更精细的锁控制。
实现一个 LockDemo 类,包含三种进阶加锁方法(tryLock / tryLock(timeout) / lockInterruptibly),并演示如何避免死锁、响应中断。
tryIncrement():if (lock.tryLock()) 获取到就 count++ 并返回 true,没获取到返回 falsetimedIncrement():lock.tryLock(1, TimeUnit.SECONDS) 最多等 1 秒,超时返回 false;处理 InterruptedExceptioninterruptLock():lock.lockInterruptibly() 等待中可被 interrupt 打断;catch 中打印提示并重设中断标志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 保证原子性
(若某线程在等待锁时被中断,会打印:等待锁的过程中被中断)
选做加分:
tryLock() / tryLock(1,SECONDS) / lockInterruptibly() 尝试拿锁,观察三种方法在"拿不到锁"时的不同表现(立即返回 false / 等 1 秒返回 false / 等待中被 interrupt 中断)tryIncrement() 外打印返回值,观察多个线程竞争锁时,拿不到锁的线程返回 false、继续执行而不阻塞——体会"避免死锁"interrupt(),观察 catch 中打印"等待锁的过程中被中断"思考题:
lock() 和 tryLock() 的区别是什么?(提示:lock() 拿不到锁就无限期等待;tryLock() 立刻尝试,拿不到立即返回 false——tryLock 是避免死锁的手段)tryLock(timeout) 的 timeout 参数是什么意思?(提示:最多等待指定时间,超时还没拿到锁就返回 false——拿不到锁不会再傻等)lockInterruptibly() 和 tryLock() 都能避免死锁吗?(提示:都能!lockInterruptibly 是"继续等但可被中断";tryLock 是"拿不到就算了"——都是 Lock 比 synchronized 更精细的控制)Thread.currentThread().interrupt() 重设中断标志?(提示:InterruptedException 抛出时会清除中断标志,重设后让上层 isInterrupted() 能感知到中断——回顾 08-10 中断机制)难度:⭐⭐⭐⭐⭐
知识点: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 张票,验证不再出现相同票 / 负数票。
private int tickets = 100;ReentrantLock lock(锁对象)sell() 卖票逻辑:if (tickets > 0) { sleep(10); 打印出售信息; tickets--; }lock.lock() → try { sell(); } finally { lock.unlock(); }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(this) { sell(); })和 Lock 写法(lock + try + finally unlock)的七维差异,写一篇对比小结思考题:
// 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 → 正常退出
}
// 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
// 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
// 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
// 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 性能最优) |