
1) 【一句话结论】:采用细粒度互斥锁(如ReentrantLock)对选课记录加锁,结合加锁顺序(如按课程ID排序)或锁升级策略,避免死锁,通过try-finally确保锁释放,保证数据一致性。
2) 【原理/概念讲解】:多线程并发修改学生选课状态时,会出现竞态条件,导致数据不一致(如多个线程同时将“待选”转为“已选”,可能超名额或状态错误)。锁机制通过互斥访问(悲观锁)或版本检查(乐观锁)保证同一时间只有一个线程修改数据。类比:银行账户取款,多个线程同时修改余额,需要锁保证只有一个线程操作,避免余额错误。
3) 【对比与适用场景】:
| 锁类型 | 定义 | 特性 | 适用场景 | 注意点 |
|---|---|---|---|---|
| 互斥锁(如ReentrantLock) | 线程独占的锁,同一时间只有一个线程持有 | 互斥,阻塞其他线程 | 需要严格保证数据一致性,如选课名额、状态 | 锁粒度过大会降低并发性能 |
| 读写锁(ReentrantReadWriteLock) | 分读锁和写锁,读锁共享,写锁互斥 | 读锁不阻塞写锁,写锁阻塞所有读/写 | 多读少写场景,如课程信息查询(读多写少) | 写操作时所有读锁释放 |
| 乐观锁(CAS) | 通过版本号或CAS操作,先检查再更新 | 无锁竞争,冲突时重试 | 写操作少,冲突概率低 | 冲突多时性能差,需重试 |
4) 【示例】:
课程类Course的选课方法伪代码:
class Course {
private int remainingSeats;
private Set<Student> selectedStudents;
private ReentrantLock lock = new ReentrantLock();
public void enroll(Student student) {
lock.lock(); // 加锁
try {
if (remainingSeats > 0) {
remainingSeats--;
selectedStudents.add(student);
student.setCourseStatus("已选");
} else {
throw new IllegalStateException("名额已满");
}
} finally {
lock.unlock(); // 确保释放锁
}
}
}
学生线程调用enroll方法,加锁后检查并修改状态,finally释放锁,保证原子性。
5) 【面试口播版答案】:
在多线程选课场景,数据不一致源于竞态条件,需用锁保证原子性。我建议用细粒度互斥锁(如ReentrantLock),对选课操作加锁,并通过try-finally确保解锁。比如,课程对象持有锁,选课时检查剩余名额,加锁后修改,释放锁。避免死锁的方法是按课程ID顺序加锁(或使用无锁的CAS,但需结合重试),确保加锁顺序一致。具体来说,每个课程实例用ReentrantLock,选课方法中加锁,检查后更新状态,finally释放锁,这样既保证一致性,又避免死锁(因为锁是按课程ID顺序获取,无循环等待)。
6) 【追问清单】:
7) 【常见坑/雷区】: