1. 并发编程中的锁机制概述
在Java并发编程领域,锁机制是保证线程安全的核心工具。作为从业多年的Java开发者,我深刻理解选择合适的锁机制对系统性能的影响。本文将深入剖析Java中两种主流锁实现——Synchronized关键字和ReentrantLock类的技术原理与实战差异。
锁的本质是控制多个线程对共享资源的访问顺序。在单核CPU时代,锁的主要作用是保证原子性;而在多核CPU架构下,锁还需要解决内存可见性和指令重排序问题。Java从1.5版本开始引入的java.util.concurrent包(JUC)彻底改变了Java并发编程的格局,其中AbstractQueuedSynchronizer(AQS)作为基础框架,为构建各种同步器提供了强大支持。
提示:理解锁机制需要掌握三个核心概念:原子性(不可分割的操作)、可见性(修改对其他线程立即可见)、有序性(指令执行顺序符合预期)。
2. AQS框架深度解析
2.1 AQS核心架构设计
AQS采用模板方法模式,将同步器的公共行为抽象出来,通过继承方式实现具体同步器。其核心设计包含三个关键部分:
-
volatile int state:同步状态标志位,不同同步器对其语义解释不同。在ReentrantLock中表示锁的持有计数;在Semaphore中表示可用许可数;在CountDownLatch中表示剩余计数。
-
CLH队列:基于Craig, Landin, and Hagersten锁队列改进的FIFO双向链表,用于管理获取资源失败的线程。每个节点(Node)保存线程引用、等待状态和前驱/后继指针。
java复制// Node类关键字段
static final class Node {
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter; // 用于条件队列
}
- ConditionObject:AQS的内部类,实现条件变量机制。每个ConditionObject维护一个独立的条件队列(单向链表),支持await/signal操作。
2.2 AQS工作流程
获取资源的核心流程(以非公平锁为例):
- 尝试直接获取锁(compareAndSetState)
- 失败后创建节点并入队
- 自旋检查前驱节点状态
- 必要时挂起线程(LockSupport.park)
释放资源的流程:
- 清除state标志
- 唤醒后继节点(LockSupport.unpark)
- 处理取消请求的节点
注意:AQS采用乐观锁思想,通过CAS(Compare-And-Swap)操作保证state变更的原子性,这是其高性能的关键。
3. ReentrantLock实现原理
3.1 锁的可重入性实现
ReentrantLock通过组合模式将同步功能委托给内部Sync类(继承AQS)。其重入特性体现在:
- 获取锁时检查当前线程是否为持有者
- 是则state递增(getState() + acquires)
- 释放时state递减,归零后完全释放
java复制final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
3.2 公平锁与非公平锁差异
ReentrantLock提供两种模式选择:
- 非公平锁(默认):新请求线程可以直接插队尝试获取锁
- 公平锁:严格遵循FIFO顺序,hasQueuedPredecessors()检查队列中是否有等待更久的线程
性能对比:
- 高竞争场景下,公平锁吞吐量比非公平锁低约10倍
- 但公平锁能避免线程饥饿问题
- 实际选择需权衡响应时间与吞吐量需求
4. Synchronized实现机制
4.1 对象头与Monitor机制
Synchronized在JVM层面实现,依赖对象头中的Mark Word存储锁状态:
- 无锁状态:存储对象hashCode、分代年龄等
- 偏向锁:存储持有线程ID
- 轻量级锁:指向栈中锁记录的指针
- 重量级锁:指向ObjectMonitor的指针
锁升级过程(不可逆):
- 初始为无锁状态
- 首次获取时升级为偏向锁(延迟启用)
- 出现竞争时转为轻量级锁(自旋)
- 自旋失败后膨胀为重量级锁(内核态阻塞)
4.2 与AQS实现的对比差异
-
性能方面:
- 低竞争时Synchronized性能更优(JVM优化)
- 高竞争时ReentrantLock更稳定(可控自旋)
-
功能特性:
特性 Synchronized ReentrantLock 可中断 × √ 超时获取 × √ 公平锁 × √ 条件变量 单一 多条件 API灵活性 低 高 -
内存消耗:
- Synchronized依赖JVM实现,无额外对象开销
- ReentrantLock需创建AQS实例(约16字节额外开销)
5. 实战选型建议与避坑指南
5.1 典型应用场景
推荐使用ReentrantLock的情况:
- 需要可中断的锁获取(避免死锁)
- 需要尝试获取锁(tryLock)
- 需要公平锁策略
- 需要绑定多个条件队列(如生产者-消费者模型)
- 需要获取锁的线程栈信息(getOwner)
推荐使用Synchronized的情况:
- 简单的临界区保护
- 低竞争率的场景
- 需要最小化内存开销时
- 与wait()/notify()配合的基础同步
5.2 常见问题排查
-
死锁预防:
- 使用tryLock设置超时
- 统一锁的获取顺序
- 使用ThreadMXBean检测死锁
-
性能调优:
- 避免在锁内执行IO操作
- 减小锁粒度(分段锁)
- 读写分离(ReentrantReadWriteLock)
-
内存泄漏:
- 确保finally块中释放锁
- 避免锁对象的逃逸
java复制// 正确使用示例
Lock lock = new ReentrantLock();
try {
lock.lock();
// 临界区代码
} finally {
lock.unlock();
}
5.3 监控与诊断技巧
-
JStack分析:
- 查看线程BLOCKED状态
- 识别锁持有者和等待者
-
JConsole监控:
- 线程争用统计
- 锁持有时间分析
-
Arthas诊断:
bash复制thread -b # 检测死锁 monitor -c 5 java.util.concurrent.locks.ReentrantLock # 监控锁竞争
在实际项目中,我曾遇到一个典型案例:使用Synchronized保护日志写入操作,在高并发时出现严重性能瓶颈。通过替换为ReentrantLock并设置适当的自旋时间,系统吞吐量提升了3倍。这印证了选择合适锁机制的重要性——没有绝对的好坏,只有适合与否。