Java学习27
·
上午 3 小时 线程安全 & 同步代码块 & 同步方法
1 线程安全问题产生原理(0.5h 精讲)
核心根源(背死)
共享资源 + 多线程抢占式调度 + 没有加锁保护
- 多个线程操作同一个共享变量
- CPU 线程随机切换,执行到一半就被抢走时间片
- 多条线程同时修改数据,造成数据覆盖、重复售卖、超卖、负数数据
经典案例:售票问题
总票数 100 张,3 个售票窗口(3 条线程)同时卖票不安全现象:
- 同一张票被多个窗口卖出
- 卖出 0 张、-1、-2 负票数
- 票数错乱、超卖
不安全完整代码(复现问题)
java
运行
// 售票任务:多个线程共享同一个任务对象
public class TicketRunnable implements Runnable {
// 共享资源:100张票
private int ticket = 100;
@Override
public void run() {
// 一直卖票
while (true) {
if (ticket > 0) {
// 模拟售票延迟,放大不安全现象
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 卖票
System.out.println(Thread.currentThread().getName()
+ " 正在售卖第 " + ticket + " 张票");
ticket--;
} else {
System.out.println("票已售罄");
break;
}
}
}
}
// 测试类
public class TicketTest {
public static void main(String[] args) {
// 同一个任务对象,多线程共享
TicketRunnable task = new TicketRunnable();
// 3个窗口线程
Thread t1 = new Thread(task, "窗口1");
Thread t2 = new Thread(task, "窗口2");
Thread t3 = new Thread(task, "窗口3");
t1.start();
t2.start();
t3.start();
}
}
代码解析
ticket=100是共享资源,三个线程共用;- 加
sleep放大线程切换时机,更容易出现重复卖票、负票; - 没加任何同步锁,CPU 随机切换,线程安全问题直接暴露。
为什么会出现负数票?
多个线程同时判断 ticket>0,都满足条件;还没执行 ticket-- 就切换线程,最后一起自减,票数被扣成负数。
2 同步代码块(核心重点 1.2h 精讲 + 代码)
格式
java
运行
synchronized(锁对象){
// 操作共享资源的核心代码(临界区)
}
核心规则
- 锁对象必须唯一:多个线程必须用同一把锁才有效;
- 同一时刻只能一个线程进入代码块;
- 其他线程在外阻塞等待,直到锁被释放;
- 锁范围越小越好,只锁操作共享资源的代码,不要锁整段。
常用锁对象
- 任意自定义对象
new Object() this- 类名.class(字节码对象,全局唯一)
用同步代码块解决售票安全问题 完整代码
java
运行
public class TicketSyncBlock implements Runnable {
private int ticket = 100;
// 定义唯一锁对象
private final Object lock = new Object();
@Override
public void run() {
while (true) {
// 同步代码块,加唯一锁
synchronized (lock) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ " 售卖第 " + ticket + " 张票");
ticket--;
} else {
System.out.println("票已售罄");
break;
}
}
}
}
}
逐行关键解析
private final Object lock = new Object();手动创建唯一锁对象,多线程共用同一把锁;synchronized (lock)进入代码块自动获取锁,执行完自动释放锁;- 保证同一时间只有一个线程能判断票数、卖票、自减,彻底解决重复票、负票。
3 同步方法(1.3h 精讲 + 代码)
格式
在方法修饰符上加 synchronized
java
运行
public synchronized void 方法名(){
// 操作共享资源代码
}
隐含锁对象(必考)
- 非静态同步方法锁对象默认是 this 当前对象
- 静态同步方法锁对象默认是 类名.class 字节码对象,全局唯一
同步方法 解决售票问题 完整代码
java
运行
public class TicketSyncMethod implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
// 调用同步卖票方法
boolean flag = saleTicket();
if (!flag) {
break;
}
}
}
// 同步方法:非静态,锁是this
public synchronized boolean saleTicket() {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ " 售卖第 " + ticket + " 张票");
ticket--;
return true;
} else {
System.out.println("票已售罄");
return false;
}
}
}
同步代码块 vs 同步方法 对比(必背表格)
表格
| 对比项 | 同步代码块 | 同步方法 |
|---|---|---|
| 锁范围 | 可自定义范围,精准控制 | 锁住整个方法,范围固定偏大 |
| 锁对象 | 可以自定义任意对象 | 非静态默认 this,静态默认类名.class |
| 灵活性 | 高,推荐使用 | 低,写法简单但粒度粗 |
| 性能 | 好,只锁核心代码 | 稍差,容易锁住多余代码 |
下午 2.5h Lock 锁 & 等待唤醒机制
1 Lock 可重入锁(1h 精讲 + 标准模板)
核心区别
synchronized:隐式锁,自动加锁、自动释放锁Lock:显式锁,手动上锁、手动解锁,可控性更强
常用实现类
ReentrantLock 可重入锁
核心方法
lock():手动获取锁unlock():手动释放锁
标准固定模板(必背)
java
运行
锁对象.lock(); // 手动上锁
try{
// 操作共享资源的代码
}finally{
锁对象.unlock(); // 无论是否异常,最终一定释放锁
}
Lock 锁 解决售票问题 完整代码
java
运行
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TicketLock implements Runnable {
private int ticket = 100;
// 创建显式锁对象
private final Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
// 手动加锁
lock.lock();
try {
if (ticket > 0) {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()
+ " 售卖第 " + ticket + " 张票");
ticket--;
} else {
System.out.println("票已售罄");
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 必须在finally中手动解锁,保证一定释放
lock.unlock();
}
}
}
}
解析重点
finally中写unlock():哪怕代码异常,也必然释放锁,不会死锁;- Lock 比 synchronized 更灵活:可尝试获取锁、可超时、可中断等待。
2 线程等待与唤醒(1.5h 精讲 + 代码)
三个核心方法(都来自 Object 类)
wait():当前线程无限等待,释放锁,进入阻塞池notify():唤醒随机一个等待中的线程notifyAll():唤醒所有等待中的线程
强制使用规则(必考)
- 必须放在 同步代码块 / 同步方法 中;
- 必须由 锁对象 调用;
- wait 会释放锁,其他线程可以抢到锁执行。
基础等待唤醒案例 完整代码
java
运行
public class WaitNotifyDemo {
// 唯一锁对象
private final Object lock = new Object();
// 等待线程
public void waitTask() {
synchronized (lock) {
System.out.println("等待线程:进入等待");
try {
// 释放锁,无限等待
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("等待线程:被唤醒,继续执行");
}
}
// 唤醒线程
public void notifyTask() {
synchronized (lock) {
System.out.println("唤醒线程:准备唤醒");
// 唤醒一个等待线程
lock.notify();
}
}
}
// 测试
class Test {
public static void main(String[] args) throws InterruptedException {
WaitNotifyDemo demo = new WaitNotifyDemo();
// 开启等待线程
new Thread(demo::waitTask).start();
Thread.sleep(2000);
// 2秒后唤醒
new Thread(demo::notifyTask).start();
}
}
3 生产者消费者模型入门
场景
生产一个资源 → 消费一个资源不能生产过剩、不能重复消费利用 wait() 满了就等待,notify() 有资源就唤醒消费
核心逻辑
- 生产者:有产品就等待,没产品就生产,生产完唤醒消费者
- 消费者:没产品就等待,有产品就消费,消费完唤醒生产者
晚上 1.5h 综合练习 + 复盘
必做实操清单
- 运行无锁售票代码,复现重复票、负票;
- 分别用 同步代码块、同步方法、Lock 锁 三种方式改造解决安全问题;
- 手写 wait/notify 等待唤醒案例,记住使用规则;
- 对比:synchronized 隐式锁 和 Lock 显式锁 优缺点。
三种同步方式优缺点总结
- 同步代码块:灵活、粒度可控、性能好,日常最常用;
- 同步方法:写法简单,锁范围大,适合整个方法都操作共享资源;
- Lock 锁:手动加解锁、功能更强、可中断 / 可超时,高并发推荐。
原计划缺失 重要知识点补充(Day27 必须掌握)
原大纲没写,但必考、后续必用,全部补上:
- 线程死锁多个线程互相持有对方需要的锁,都不释放,无限等待,程序卡死;产生条件 + 如何规避。
- wait () 带参重载
wait(毫秒):限时等待,时间到自动唤醒,不用别人 notify。 - ReentrantLock 可重入含义同一线程可多次获取同一把锁,不会自己卡死自己。
- 虚假唤醒wait 可能被意外唤醒,开发规范必须 while 循环判断条件,不能用 if。
- 锁的范围与性能损耗锁范围越大,并发越低,尽量缩小临界区代码。
Day27 验收标准(强化版)
- 能说出线程安全问题产生根本原因,能复现售票错乱现象;
- 熟练手写同步代码块、同步方法,分清各自默认锁对象;
- 会用 ReentrantLock 标准 try-finally 模板加锁解锁;
- 熟记 wait/notify/notifyAll 作用 + 三条强制使用规则;
- 能区分三种同步方式特点、适用场景;
- 了解死锁产生原因、虚假唤醒规范写法。
Day27 学习总结(带对比表格 + 核心结论)
一、线程安全问题根源
多线程共享同一个资源 + 抢占式调度 + 无锁保护导致:重复卖票、超卖、卖出负数票。
二、三种加锁方式核心对比表
表格
| 对比维度 | 同步代码块 synchronized | 同步方法 synchronized | Lock 锁 ReentrantLock |
|---|---|---|---|
| 锁类型 | 隐式锁,自动加锁自动释放 | 隐式锁,自动加锁自动释放 | 显式锁,手动 lock ()、手动 unlock () |
| 锁对象 | 自己指定任意唯一对象 | 非静态:this静态:类名.class | 自己 new ReentrantLock |
| 锁范围 | 灵活,只锁核心代码 | 锁住整个方法,范围偏大 | 灵活,自己控制加锁解锁范围 |
| 写法复杂度 | 中等 | 最简单 | 稍复杂,必须 try-finally 保证解锁 |
| 性能并发 | 较好,锁粒度小 | 一般,容易锁多余代码 | 更好,功能更强、可扩展 |
| 可控性 | 不可手动控制 | 不可手动控制 | 可中断、可超时、可尝试拿锁 |
| 解决霸占问题 | 锁外循环加 sleep (10) | 锁外循环加 sleep (10) | 锁外循环加 sleep (10) |
三、三个方式共性(必背)
- 都能解决线程安全,杜绝重复票、负数票;
- 同一时间只能一个线程进临界区;
- 不加「锁外 sleep」,先启动的线程会一直抢占霸占;
- 统一解决霸占方案:while 循环里、锁代码外面 加 Thread.sleep (10),让出 CPU,实现多窗口交替执行。
四、三者区别关键
- 同步代码块:灵活,推荐日常用,可精准控制锁范围;
- 同步方法:写法最懒,但锁整个方法,粒度粗、并发低;
- Lock 锁:手动加解锁,功能最强,高并发项目首选。
五、今天踩坑总结
- 线程启动顺序决定谁第一张票先卖;
- 代码执行太快,同一线程立刻循环抢锁,会一直霸占;
sleep加在锁里面是模拟业务耗时;加在锁外面是礼让其他线程,实现交替;- 同步方法默认锁是
this,静态同步方法锁是类名.class。
更多推荐



所有评论(0)