1. Java并发包中的管程:Lock和Condition
1.1. Lock 的核心意义:再造管程
Java 原生的 synchronized
关键字已经是管程的一种实现,那为什么还要提供 Lock
接口?
原因是:
Lock
弥补了 synchronized
的三大缺陷,尤其是在解决“死锁”中的不可抢占条件时具有更强的灵活性。
三种解决“不可抢占”的能力(也是 Lock 的优势):
1. 可中断获取锁
void lockInterruptibly() throws InterruptedException;
- 遇到死锁时可通过中断来释放已有资源。
2. 支持超时的获取锁
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
- 限时等待,避免无限期阻塞。
3. 非阻塞地获取锁
boolean tryLock();
- 获取不到立即返回,避免线程挂起。
1.2. Lock 的可见性保障原理
问题:
在使用 Lock
的并发场景中,如何确保对共享变量修改的可见性?
解决方案:
基于 Java 内存模型的 Happens-Before 原则 和 ReentrantLock
内部的 volatile state 实现:
- 解锁前后的顺序性保证(线程内)
- volatile 变量规则(state 的读写)
- 传递性规则:T1 修改变量 → 解锁 → T2 加锁 → 可见
1.3. 可重入锁(ReentrantLock)、公平锁与非公平锁
1. 可重入锁(ReentrantLock)
可重入锁的定义:指的是线程可以重复获取同一把锁。
示例:
例如下面代码中,当线程 T1 执行到 ① 处时,已经获取到了锁 rtl ,当在 ① 处调用 get() 方法时,会在 ② 再次对锁 rtl 执行加锁操作。此时,如果锁 rtl 是可重入的,那么线程 T1 可以再次加锁成功;如果锁 rtl 是不可重入的,那么线程 T1 此时会被阻塞。
class X {
private final Lock rtl =
new ReentrantLock();
int value;
public int get() {
// 获取锁
rtl.lock(); ②
try {
return value;
} finally {
// 保证锁能释放
rtl.unlock();
}
}
public void addOne() {
// 获取锁
rtl.lock();
try {
value = 1 + get(); ①
} finally {
// 保证锁能释放
rtl.unlock();
}
}
}
- 可重入锁可避免同一个线程因为重复获取锁而造成死锁。
可重入函数(线程安全的):
重入函数,指的是多个线程可以同时调用该函数,每个线程都能得到正确结果;同时在一个线程内支持线程切换,无论被切换多少次,结果都是正确的。
2. 公平锁与非公平锁
区别:
- 公平锁:先到先得,排队唤醒,避免线程“饿死”
- 非公平锁(默认):允许插队,有更好的性能表现
// 无参构造函数:默认非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
// 根据公平策略参数创建锁
public ReentrantLock(boolean fair){
sync = fair ? new FairSync()
: new NonfairSync();
}
1.4. 用锁的最佳实践
最佳实践(出自 Doug Lea):
- 只在更新对象的成员变量时加锁
- 只在访问可变成员变量时加锁
- 不要在加锁后调用其他对象的方法
-
- 可能造成不可控延迟(例如 IO、sleep)
- 可能加其他锁,引发死锁风险
补充建议:
- 减少锁的持有时间(快速释放)
- 降低锁的粒度(尽可能小范围加锁)
- 避免锁的嵌套调用
总结:
Lock
提供了比synchronized
更丰富和灵活的控制机制。- 使用 Lock 编写并发程序时,应牢记:
-
- 能不能中断?
- 能不能超时?
- 能不能快速失败?
- 最好的并发代码是简单、清晰、易于分析的。
1.5. Condition的概念
Lock 与 synchronized 的区别:
- 支持中断响应(
lock.lockInterruptibly()
) - 支持超时获取锁(
tryLock(timeout)
) - 支持非阻塞获取锁(
tryLock()
)
Condition 是什么?
- 管程中的条件变量。
- 与
Object.wait/notify
相比,Condition
支持多个条件变量,适用于更复杂的并发场景(如阻塞队列:notEmpty
、notFull
)。
Condition 使用示例:阻塞队列
public class BlockedQueue<T> {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition(); // 队列不满
final Condition notEmpty = lock.newCondition(); // 队列不空
void enq(T x) {
lock.lock();
try {
while (队列已满) {
notFull.await();
}
// 执行入队操作...
notEmpty.signal(); // 通知可以出队
} finally {
lock.unlock();
}
}
void deq() {
lock.lock();
try {
while (队列为空) {
notEmpty.await();
}
// 执行出队操作...
notFull.signal(); // 通知可以入队
} finally {
lock.unlock();
}
}
}
- 注意:Lock 和 Condition 实现的管程,线程等待和通知需要调用 await()、signal()、signalAll(),它们的语义和 wait()、notify()、notifyAll() 是相同的。
1.6. 异步与同步的本质
同步: 调用方必须等待结果返回(如函数调用阻塞当前线程)。
异步: 调用方无需等待结果,调用立即返回,后续结果通过回调或其他方式通知。
举例:
pai1M(); // 如果阻塞等结果 -> 同步
printf("hello world"); // 若立即执行,不等待上面 -> 异步
实现异步的两种方式:
- 调用方创建线程: 在子线程中调用(常见于主线程不被阻塞的情况)。
- 被调用方创建线程: 方法中主动异步处理逻辑,主线程立即 return。
1.7. 异步转同步
异步转同步:
调用本来是异步执行的,但我们人为“阻塞住”当前线程,等它的结果返回后再继续执行,从而模拟出同步调用的效果。
知识点 |
说明 |
异步调用 |
不等待结果,直接返回 |
同步调用 |
等待结果,阻塞线程 |
Dubbo 的异步转同步 |
发送请求是异步的,但通过 |
核心实现 |
|
好处 |
兼顾性能(异步发送)和易用性(同步获取结果) |
1.8. Lock-Condition例子
示例功能:
- 有一个共享队列;
- 生产者线程往队列里放数据;
- 消费者线程从队列中取数据;
- 使用
ReentrantLock
和Condition
实现线程间的等待和唤醒机制。
Java代码:
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockConditionExample {
// 定义一个固定大小的共享队列
private static final int CAPACITY = 5;
private final Queue<Integer> queue = new LinkedList<>();
// 显式锁对象
private final Lock lock = new ReentrantLock();
// 队列满时:生产者等待的条件
private final Condition notFull = lock.newCondition();
// 队列空时:消费者等待的条件
private final Condition notEmpty = lock.newCondition();
// 生产者方法
public void produce(int value) throws InterruptedException {
lock.lock(); // 获取锁
try {
// 如果队列已满,则等待 notFull 条件
while (queue.size() == CAPACITY) {
System.out.println("队列满了,生产者等待...");
notFull.await(); // 进入等待并释放锁
}
// 加入元素到队列
queue.offer(value);
System.out.println("生产者生产了: " + value);
// 通知消费者:队列非空
notEmpty.signal();
} finally {
lock.unlock(); // 释放锁
}
}
// 消费者方法
public int consume() throws InterruptedException {
lock.lock(); // 获取锁
try {
// 如果队列为空,则等待 notEmpty 条件
while (queue.isEmpty()) {
System.out.println("队列空了,消费者等待...");
notEmpty.await(); // 进入等待并释放锁
}
// 从队列中取出一个元素
int value = queue.poll();
System.out.println("消费者消费了: " + value);
// 通知生产者:队列不满了
notFull.signal();
return value;
} finally {
lock.unlock(); // 释放锁
}
}
// 主函数 - 启动生产者和消费者线程
public static void main(String[] args) {
LockConditionExample example = new LockConditionExample();
// 生产者线程
Thread producer = new Thread(() -> {
int value = 0;
try {
while (true) {
example.produce(value++);
Thread.sleep(500); // 模拟生产速度
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// 消费者线程
Thread consumer = new Thread(() -> {
try {
while (true) {
example.consume();
Thread.sleep(1000); // 模拟消费速度
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// 启动线程
producer.start();
consumer.start();
}
}
疑问:
它们共享一个锁,但它们仍然能「协作式」地工作。不是并行执行临界区的代码,而是轮流进入、协同完成任务。
这意味着:
同一时刻只能有一个线程(无论是生产者还是消费者)进入加锁的代码块,也就是说:
- 当生产者线程进入
produce()
方法时,消费者线程如果也想进入consume()
方法,就必须等待。 - 同样,反过来也一样。
那为什么说它们“可以协同工作”?
因为它们用 Condition
做了同步等待与唤醒:
具体情况如下:
场景 |
行为 |
队列满时 |
生产者执行 |
队列有空位了 |
消费者 |
队列空时 |
消费者执行 |
队列有数据了 |
生产者 |
- 所以虽然它们使用同一个锁,但通过
Condition
的等待和唤醒机制,实现了“互不干扰”的轮流工作流程 —— 类似于两个人轮流进一个房间传递东西,每次只能一个人进去,但彼此不会阻碍工作流程。