准备面试时,Java锁的种类和特性总是让人头疼:synchronized和Lock的区别、公平锁与非公平锁的实现、各种并发工具类背后的锁机制……这篇文章系统整理了Java中常见的锁类型,从基础概念到实现原理,再到使用场景,帮你一网打尽锁相关知识点。
一、锁的基本分类
按不同维度可将Java中的锁分为以下几类:
分类维度 | 具体类型 |
---|---|
获取方式 | 隐式锁(synchronized)、显式锁(Lock接口实现类) |
竞争策略 | 公平锁、非公平锁 |
共享方式 | 独占锁、共享锁 |
优化机制 | 偏向锁、轻量级锁、重量级锁(synchronized的锁升级) |
功能特性 | 可重入锁、可中断锁、读写锁、自旋锁、阻塞锁、互斥锁、分段锁、分布式锁等 |
二、基础锁:synchronized详解
synchronized
是Java内置的隐式锁,无需手动释放,是线程安全的基础保障。
2.1 实现原理
- 同步代码块:通过
monitorenter
和monitorexit
指令实现,编译后在代码块前后插入这两个指令 - 同步方法:通过方法修饰符上的
ACC_SYNCHRONIZED
标志实现 - 本质:依赖对象头中的
Mark Word
存储锁状态,关联Monitor对象实现线程排队
2.2 锁升级过程
JDK 1.6对synchronized
进行了优化,引入了锁升级机制,避免每次都使用重量级锁:
- 无锁状态:对象刚创建时,未被任何线程锁定
- 偏向锁:优先偏向第一个获取锁的线程,若后续无竞争,该线程可直接获取锁(通过CAS修改Mark Word)
- 轻量级锁:当有其他线程竞争时,升级为轻量级锁,线程通过自旋尝试获取锁(避免阻塞)
- 重量级锁:自旋达到一定次数或线程数过多时,升级为重量级锁,依赖操作系统的互斥量实现(会导致线程阻塞)
2.3 使用示例
// 同步方法
public synchronized void syncMethod() {
// 线程安全的操作
}
// 同步代码块
public void syncBlock() {
synchronized (this) { // 锁对象可以是this、类对象或其他对象
// 线程安全的操作
}
}
三、显式锁:Lock接口及其实现
java.util.concurrent.locks.Lock
接口提供了更灵活的锁操作,需要手动获取和释放。
3.1 Lock接口核心方法
public interface Lock {
void lock(); // 获取锁,阻塞式
void lockInterruptibly() throws InterruptedException; // 可中断地获取锁
boolean tryLock(); // 尝试获取锁,立即返回结果
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 超时尝试获取锁
void unlock(); // 释放锁(必须在finally中调用)
Condition newCondition(); // 创建条件变量
}
3.2 ReentrantLock:可重入锁
ReentrantLock
是Lock接口的最常用实现,支持可重入性(同一线程可多次获取锁)。
3.2.1 公平锁与非公平锁
// 公平锁:按线程请求顺序获取锁
Lock fairLock = new ReentrantLock(true);
// 非公平锁:允许线程"插队"获取锁(默认)
Lock nonfairLock = new ReentrantLock(false);
- 公平锁:优点是避免线程饥饿,缺点是性能较差(需要维护等待队列)
- 非公平锁:优点是性能好,缺点是可能导致线程饥饿(少数线程长期获取不到锁)
3.2.2 使用示例
Lock lock = new ReentrantLock();
try {
lock.lock(); // 获取锁
// 线程安全的操作
} finally {
lock.unlock(); // 必须释放锁
}
3.3 读写锁:ReentrantReadWriteLock
读写锁将锁分为读锁(共享锁)和写锁(独占锁),支持多线程同时读,但写时互斥。
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock = rwLock.readLock(); // 读锁(共享)
Lock writeLock = rwLock.writeLock(); // 写锁(独占)
// 读操作示例
readLock.lock();
try {
// 读取数据
} finally {
readLock.unlock();
}
// 写操作示例
writeLock.lock();
try {
// 修改数据
} finally {
writeLock.unlock();
}
适用场景:读多写少的场景(如缓存、配置中心),可显著提高并发性能。
四、特殊功能的锁
4.1 自旋锁
线程获取锁失败时不会立即阻塞,而是通过循环不断尝试获取锁,减少线程上下文切换开销。
public class SpinLock {
private AtomicReference<Thread> owner = new AtomicReference<>();
public void lock() {
Thread current = Thread.currentThread();
// 自旋尝试获取锁
while (!owner.compareAndSet(null, current)) {
// 空循环,不断重试
}
}
public void unlock() {
Thread current = Thread.currentThread();
owner.compareAndSet(current, null);
}
}
适用场景:锁持有时间短、线程数少的场景(如JDK中的Unsafe
类实现)。
4.2 阻塞锁
与自旋锁相反,线程获取锁失败时会进入阻塞状态,等待被唤醒。
// synchronized和ReentrantLock的重量级锁都是阻塞锁
synchronized (this) {
// 阻塞锁保护的代码
}
适用场景:锁持有时间长、线程数多的场景。
4.3 可中断锁
支持中断机制,线程在等待锁的过程中可被中断,避免无限等待。
Lock lock = new ReentrantLock();
try {
// 可中断地获取锁
lock.lockInterruptibly();
} catch (InterruptedException e) {
// 处理中断(如恢复中断状态、退出操作)
Thread.currentThread().interrupt();
} finally {
if (lock.tryLock()) { // 检查是否持有锁
lock.unlock();
}
}
4.4 分段锁
将锁的范围细分,不同分段使用不同的锁,提高并发度(如ConcurrentHashMap
)。
// JDK 7中ConcurrentHashMap的分段锁实现
Segment<K,V>[] segments; // 分段数组,每个分段是一个可重入锁
// 操作时只锁定对应分段
Segment<K,V> s = segments[hash];
s.lock();
try {
// 操作当前分段的数据
} finally {
s.unlock();
}
五、并发工具类中的锁机制
5.1 CountDownLatch:倒计时锁
基于共享锁实现,允许一个或多个线程等待其他线程完成操作。
// 初始化计数器为3
CountDownLatch latch = new CountDownLatch(3);
// 线程1
new Thread(() -> {
try {
// 执行任务
} finally {
latch.countDown(); // 计数器减1
}
}).start();
// 等待线程:等待计数器变为0
latch.await(); // 阻塞直到所有任务完成
5.2 CyclicBarrier:循环屏障
基于独占锁实现,让多个线程相互等待,直到所有线程都到达屏障点。
// 初始化5个线程的屏障,到达后执行回调
CyclicBarrier barrier = new CyclicBarrier(5, () -> {
System.out.println("所有线程已到达,执行汇总操作");
});
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
// 执行任务
barrier.await(); // 等待其他线程
// 所有线程到达后继续执行
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
5.3 Semaphore:信号量
基于共享锁实现,控制同时访问特定资源的线程数量。
// 允许3个线程同时访问
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
semaphore.acquire(); // 获取许可
// 访问受限资源
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 释放许可
}
}).start();
}
六、分布式锁
在分布式系统中,多个节点间的锁机制,常用实现方式:
-
Redis分布式锁:基于
SET NX
命令实现// 获取锁:不存在则设置,过期时间30秒 Boolean locked = redisTemplate.opsForValue().setIfAbsent("lock:key", "value", 30, TimeUnit.SECONDS); // 释放锁:需要判断是否为当前线程持有(防止误释放) if (locked != null && locked) { redisTemplate.delete("lock:key"); }
-
ZooKeeper分布式锁:基于临时节点和Watcher机制实现
// 使用Curator框架的InterProcessMutex InterProcessMutex lock = new InterProcessMutex(zkClient, "/lock/path"); try { lock.acquire(); // 获取锁 // 执行分布式任务 } finally { lock.release(); // 释放锁 }
-
数据库分布式锁:基于
SELECT ... FOR UPDATE
的行锁实现
七、锁的选择与对比
锁类型 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
synchronized | 简单易用、自动释放、JVM优化好 | 灵活性低、功能少 | 简单的线程安全场景 |
ReentrantLock | 灵活、可中断、支持公平锁、条件变量 | 需要手动释放、代码繁琐 | 复杂的并发控制(如超时、中断、公平性需求) |
读写锁 | 读操作并发高 | 写操作性能差、实现复杂 | 读多写少的场景(如缓存) |
分布式锁 | 跨节点同步 | 实现复杂、性能开销大 | 分布式系统中的资源竞争 |
八、面试高频问题
-
synchronized和ReentrantLock的区别?
- 前者是隐式锁,后者是显式锁
- 前者不支持中断和超时,后者支持
- 前者默认非公平,后者可选择公平/非公平
- 前者无条件变量,后者通过Condition实现
- 前者锁升级机制更高效,后者灵活性更高
-
什么是可重入锁?为什么需要可重入性?
- 可重入锁允许同一线程多次获取同一把锁,避免死锁
- 例如:同步方法调用另一个同步方法时,无需重新获取锁
-
公平锁和非公平锁的优缺点?
- 公平锁:优点是避免饥饿,缺点是性能差
- 非公平锁:优点是性能好,缺点是可能导致饥饿
- 大多数场景选择非公平锁(默认),除非有特殊公平性需求
-
读写锁适合什么场景?为什么?
- 适合读多写少的场景(如缓存、配置)
- 读操作共享,写操作独占,平衡并发性能和数据一致性
-
锁升级的过程?为什么要设计锁升级?
- 无锁→偏向锁→轻量级锁→重量级锁
- 目的是减少锁的性能开销,不同竞争强度用不同锁机制
掌握这些锁的特性和使用场景,不仅能在面试中应对自如,更能在实际开发中选择合适的锁机制,写出高效且线程安全的代码。锁是并发编程的基础,理解透彻后,面对复杂的并发问题也能游刃有余。