一、synchronized 关键字
1. 基本用法
-
修饰实例方法:锁是当前对象实例。
public synchronized void method() { // 同步代码 }
-
修饰静态方法:锁是当前类的
Class
对象。public static synchronized void staticMethod() { // 同步代码 }
-
修饰代码块:显式指定锁对象。
public void blockMethod() { synchronized (lockObject) { // 同步代码 } }
2. 底层原理
-
对象头与 Monitor:
- 每个Java对象在堆中都有一个对象头,包含
Mark Word
(存储锁状态和线程ID)。 Monitor
(管程)是JVM实现的互斥锁机制,通过monitorenter
和monitorexit
指令控制锁的获取和释放。
- 每个Java对象在堆中都有一个对象头,包含
-
锁升级过程(JDK6优化):
- 无锁:初始状态。
- 偏向锁:仅一个线程访问时,通过CAS记录线程ID。
- 轻量级锁:多个线程竞争但未冲突时,通过CAS自旋尝试获取锁。
- 重量级锁:竞争激烈时,升级为操作系统级互斥锁(涉及内核态切换)。
3. 特性与限制
- 可重入性:同一线程可重复获取同一锁。
- 非公平锁:不保证等待线程的获取顺序。
- 自动释放:代码块结束或异常时自动释放锁。
4. 适用场景
- 简单的临界区保护(如单例模式)。
- 低竞争环境(避免重量级锁的性能损耗)。
二、AQS(AbstractQueuedSynchronizer)
1. 核心设计
-
作用:Java并发包(
java.util.concurrent.locks
)的基础框架,用于构建锁和同步器(如ReentrantLock
、Semaphore
)。 -
核心组件:
- 状态变量(
state
) :通过CAS操作控制同步状态(如锁的持有次数)。 - CLH队列:双向链表实现的等待队列,管理竞争线程。
- 状态变量(
-
模板方法模式:子类需实现
tryAcquire
、tryRelease
等钩子方法。 AQS(AbstractQueuedSynchronizer)是 Java 并发包中的一个核心类,它大量运用了模板方法模式。以下是关于 AQS 中模板方法模式的详细介绍:
模板方法模式概述
模板方法模式是一种行为设计模式,它在一个抽象类中定义了一个算法的骨架,而将一些步骤延迟到子类中实现。这样可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。
AQS 中的模板方法模式体现
-
AQS 中的抽象模板方法
acquire(int arg)
:这是 AQS 提供的一个模板方法,用于获取锁等同步状态。它的实现依赖于其他几个方法,如tryAcquire(int arg)
、addWaiter(Node mode)
、acquireQueued(Node node, int arg)
等。acquire
方法定义了获取同步状态的整体流程框架,而具体的获取逻辑由子类实现的tryAcquire
方法来完成。release(int arg)
:用于释放锁等同步状态的模板方法。它同样依赖于tryRelease(int arg)
等方法,release
方法定义了释放同步状态的通用流程,而具体的释放逻辑由子类实现的tryRelease
方法来决定。
-
AQS 中的具体模板方法执行流程
-
以
acquire
方法为例- 首先,调用
tryAcquire
方法尝试获取同步状态。这个方法是留给子类实现的,不同的子类可以根据自身的需求来定义具体的获取逻辑。 - 如果
tryAcquire
方法返回false
,表示获取同步状态失败,则会通过addWaiter
方法将当前线程包装成一个节点,并添加到等待队列中。 - 然后,通过
acquireQueued
方法让线程在队列中等待,不断尝试获取同步状态,直到获取成功或者线程被中断等情况发生。
- 首先,调用
-
以
release
方法为例- 首先,调用
tryRelease
方法尝试释放同步状态,这也是由子类来实现具体逻辑。 - 如果
tryRelease
方法返回true
,表示释放成功,则会从等待队列中唤醒一个等待的线程,让其有机会获取同步状态。
- 首先,调用
-
AQS 中模板方法模式的作用
- 提高代码复用性:AQS 通过模板方法模式,将获取和释放同步状态等通用的逻辑封装在抽象类中,子类只需要实现具体的获取和释放逻辑,避免了大量重复代码的编写。
- 增强可扩展性:当需要实现新的同步器时,只需要继承 AQS 并实现相应的抽象方法即可,符合开闭原则,即对扩展开放,对修改关闭。
- 实现统一的同步框架:使得 Java 并发包中的各种同步器,如
ReentrantLock
、Semaphore
、CountDownLatch
等,都能够基于 AQS 提供的统一模板方法来实现,保证了同步器的一致性和稳定性。
2. 工作流程
-
获取锁:
- 调用
acquire(int arg)
,先尝试tryAcquire
。 - 失败后,将线程加入CLH队列并阻塞。
- 调用
-
释放锁:
- 调用
release(int arg)
,先执行tryRelease
。 - 唤醒队列中的后继线程。
- 调用
3. 典型实现
- ReentrantLock:基于AQS的可重入锁。
- CountDownLatch:基于AQS的倒计数器。
三、ReentrantLock
1. 核心特性
-
显式锁:需手动
lock()
和unlock()
,配合try-finally
使用。ReentrantLock lock = new ReentrantLock(); lock.lock(); try { // 临界区代码 } finally { lock.unlock(); }
-
可重入性:同一线程可多次获取锁(通过
state
记录重入次数)。 -
公平性:支持公平锁(按等待顺序获取)和非公平锁(默认)。
ReentrantLock fairLock = new ReentrantLock(true); // 公平锁
-
可中断性:通过
lockInterruptibly()
响应中断。 -
条件变量:通过
Condition
实现线程等待/唤醒。Condition condition = lock.newCondition(); condition.await(); // 等待 condition.signal(); // 唤醒
2. 与 synchronized 对比
特性 | synchronized | ReentrantLock |
---|---|---|
锁获取方式 | 隐式(自动释放) | 显式(需手动释放) |
公平性 | 非公平 | 支持公平和非公平 |
可中断性 | 不支持 | 支持(lockInterruptibly() ) |
条件变量 | 通过 wait() /notify() | 通过 Condition 实现更灵活控制 |
性能 | JDK6优化后接近 | 高竞争场景下更灵活 |
3. 适用场景
- 需要细粒度控制(如超时、公平性、条件等待)。
- 高并发竞争环境(通过自旋和CAS减少阻塞)。
四、锁的底层实现对比
锁类型 | 实现机制 | 性能特点 |
---|---|---|
synchronized | JVM内置锁(Monitor + 对象头) | 低竞争时性能优,重量级锁有性能损耗 |
ReentrantLock | 基于AQS + CAS + CLH队列 | 高竞争时更灵活,支持公平锁 |
五、常见问题与解决方案
1. 死锁
-
场景:多个线程互相等待对方释放锁。
-
排查工具:
jstack
:查看线程堆栈,识别死锁链。- Arthas:
thread -b
直接定位死锁。
-
解决:按固定顺序获取锁,或使用
tryLock()
超时机制。
2. 锁竞争性能瓶颈
-
场景:高并发下锁竞争导致吞吐量下降。
-
优化手段:
- 减小锁粒度:如
ConcurrentHashMap
的分段锁。 - 无锁编程:使用
CAS
(如AtomicInteger
)。 - 读写锁:
ReentrantReadWriteLock
分离读/写锁。
- 减小锁粒度:如
3. 锁饥饿
- 场景:公平锁中低优先级线程长期无法获取锁。
- 解决:合理设置线程优先级,或使用非公平锁。
六、最佳实践
-
优先使用 synchronized:
- 简单场景下,利用JVM优化(锁升级)。
-
复杂场景选择 ReentrantLock:
- 需要可中断、超时、公平性等高级特性时。
-
避免锁嵌套:
- 减少死锁风险,保持锁顺序一致。
-
锁分离与合并:
- 读写分离(
ReadWriteLock
)、锁粗化(合并相邻同步块)。
- 读写分离(
七、示例代码
1. 使用 ReentrantLock 实现线程安全计数器
public class SafeCounter {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
2. 条件变量实现生产者-消费者模型
public class ProducerConsumer {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
private final Condition notFull = lock.newCondition();
private Queue<Integer> queue = new LinkedList<>();
private int maxSize = 10;
public void produce(int value) throws InterruptedException {
lock.lock();
try {
while (queue.size() == maxSize) {
notFull.await();
}
queue.add(value);
notEmpty.signal();
} finally {
lock.unlock();
}
}
public int consume() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await();
}
int value = queue.poll();
notFull.signal();
return value;
} finally {
lock.unlock();
}
}
}
总结
- synchronized:简单易用,适合低竞争场景,依赖JVM优化。
- AQS:Java并发包的核心框架,提供灵活的同步器扩展能力。
- ReentrantLock:功能丰富,支持公平锁、可中断锁和条件变量,适合高竞争复杂场景。
- 选择建议:根据业务需求权衡易用性、性能和功能扩展性,优先选择最简方案。