真实面经题目 · 原创解析
java中加锁的方式有哪些,怎么个写法?
Java 加锁常见方式包括 synchronized、ReentrantLock、ReadWriteLock、StampedLock,以及更轻量的原子类、并发容器、线程封闭等替代方案。面试回答不能只背 API,重点要说清楚锁住的对象、写法、释放方式、适用场景、风险和性能取舍。
真实面经题目 · 原创解析
Java 加锁常见方式包括 synchronized、ReentrantLock、ReadWriteLock、StampedLock,以及更轻量的原子类、并发容器、线程封闭等替代方案。面试回答不能只背 API,重点要说清楚锁住的对象、写法、释放方式、适用场景、风险和性能取舍。
可以按“内置锁、显式锁、读写锁、乐观读锁、少用锁的替代方案”来回答。synchronized 是 JVM 内置锁,可以修饰实例方法、静态方法,也可以包住代码块;ReentrantLock 是显式锁,必须在 finally 中 unlock,适合需要可中断、超时尝试、公平锁、多个 Condition 的场景;ReentrantReadWriteLock 适合读多写少,把读锁和写锁分开;StampedLock 支持乐观读,适合读多写少且读操作很短的场景,但它不可重入,使用复杂度更高。实际开发中不能为了线程安全就到处加锁,要优先缩小共享状态范围,能用不可变对象、局部变量、并发集合、原子类、CAS、队列串行化解决的,就不要引入粗粒度互斥锁。
加锁的本质是保护共享可变状态,而不是保护代码本身。面试中要先说明:多个线程是否会同时访问同一份数据;是否存在写操作;是否要求复合操作的原子性;锁的粒度是对象级、类级、字段级还是业务资源级。只有明确保护对象,才能判断应该锁 this、Class、私有锁对象,还是使用显式锁。
synchronized 是 Java 内置监视器锁。修饰实例方法时,锁的是当前对象 this;修饰 static 方法时,锁的是当前类的 Class 对象。它的优点是语法简单、自动释放、异常时不会忘记解锁,并且具备可见性和互斥性。缺点是灵活性不如显式锁,不能直接做超时获取锁、公平锁选择、多个条件队列等复杂控制。
synchronized 代码块可以精确控制临界区和锁对象。常见写法是 synchronized(lockObject) { ... }。工程上更推荐使用 private final Object lock = new Object() 作为锁对象,避免锁 this 或公共对象导致外部代码也能参与竞争。代码块的优势是粒度更小,只把真正访问共享状态的部分包起来,减少锁竞争。
ReentrantLock 是 java.util.concurrent.locks 包下的可重入显式锁。典型写法是 lock.lock(); try { 临界区 } finally { lock.unlock(); }。它适合需要高级控制的场景,例如 tryLock 避免死等、tryLock(timeout) 超时放弃、lockInterruptibly 响应中断、公平锁构造、配合 Condition 拆分等待队列。代价是必须手动释放锁,遗漏 finally 会造成严重故障。
ReadWriteLock 的典型实现是 ReentrantReadWriteLock,核心思想是读读不互斥、读写互斥、写写互斥。它适合读多写少、读操作耗时明显、共享数据一致性要求明确的场景。写法上使用 readLock().lock() 保护读路径,使用 writeLock().lock() 保护写路径。需要注意读写锁不是万能优化,如果写很多、临界区很短、锁竞争不明显,读写锁反而可能更复杂更慢。
StampedLock 提供写锁、悲观读锁和乐观读。乐观读通常先 tryOptimisticRead 获取 stamp,读取字段后 validate(stamp) 校验期间是否发生写入;校验失败再退化为悲观读锁。它适合读多写少、读操作短、可以容忍重试的场景。它的限制也很重要:不可重入,使用 stamp 解锁,写法容易出错,不适合简单业务中随意替代 synchronized 或 ReentrantLock。
并发设计不应该把加锁作为唯一答案。能用局部变量、不可变对象、ThreadLocal、ConcurrentHashMap、BlockingQueue、AtomicInteger、LongAdder、CopyOnWriteArrayList、数据库唯一约束或消息队列串行化解决的问题,通常不需要手写互斥锁。锁的粒度过大容易降低吞吐,锁的顺序混乱容易死锁,锁住慢 IO 或远程调用会放大故障影响。
class LockExamples {
private final Object monitor = new Object();
private final java.util.concurrent.locks.ReentrantLock lock =
new java.util.concurrent.locks.ReentrantLock();
private final java.util.concurrent.locks.ReadWriteLock rw =
new java.util.concurrent.locks.ReentrantReadWriteLock();
public synchronized void syncMethod() {
// lock: this
}
public static synchronized void staticSyncMethod() {
// lock: LockExamples.class
}
public void syncBlock() {
synchronized (monitor) {
// lock: private final monitor
}
}
public void reentrantLockStyle() {
lock.lock();
try {
// critical section
} finally {
lock.unlock();
}
}
public void readWriteStyle() {
rw.readLock().lock();
try {
// read shared state
} finally {
rw.readLock().unlock();
}
rw.writeLock().lock();
try {
// mutate shared state
} finally {
rw.writeLock().unlock();
}
}
} 普通互斥优先 synchronized,代码短、自动释放、可读性好。需要超时获取锁、可中断加锁、公平锁、多个 Condition 队列,或者需要更精细的锁控制时,选择 ReentrantLock。
this 是外部可见对象,外部代码也可能 synchronized 同一个实例,导致意外阻塞或死锁。更稳妥的方式是使用 private final Object lock 作为内部锁对象。
同一个线程已经持有某把锁时,可以再次获取这把锁而不会被自己阻塞。synchronized 和 ReentrantLock 都是可重入锁。可重入能力常用于一个同步方法调用另一个同步方法的场景。
不一定。它只在读多写少、读操作较重、竞争明显时更可能有收益。如果临界区很小、写操作多或锁竞争低,读写锁的管理成本可能超过收益。
能。线程释放锁前对共享变量的修改,对之后获取同一把锁的线程可见。关键是必须围绕同一把锁访问同一份共享状态,否则可见性和互斥都不成立。
常见做法是固定加锁顺序、减少锁嵌套、缩小临界区、不在持锁期间调用外部服务或未知回调,必要时使用 tryLock 超时失败后释放已持有的锁。