管程是一种管理共享变量以及对共享变量的操作过程的技术,在各种高级语言中都有实现
管程和信号量机制
操作系统原理指出信号量机制可以解决所以并发问题,而管程和信号量是可以相互实现的
管程相对而言更容易使用,Java 选择了管程来支持并发技术,synchronized 就是一个管程原语
管程的模型
Java 中采用的管程模型是目前广泛使用的 MESA 模型
管程同一时刻只能允许一个线程进入,其他线程在入口等待队列中等待
持有管程的线程在执行过程中,发现某些条件不符合,就会进入该条件的条件等待队列
当然,如果线程发现某些条件已经符合,可以唤醒相应条件等待队列中的其他线程,重新进入入口队列等待
管程解决的问题
互斥:提供入口队列保证线程单一的进入管程
同步:提供条件等待和唤醒机制作为线程间通信手段
Java 中的管程
JDK 提供了 synchronized 关键字和 Lock API 两种管程实现
synchronized 的 ObjectMonitor 机制
synchronized 是 JVM 层面的管程原语,可以对任意对象加锁
任意 Java 对象上都有三个容器用来实现管程,分别是 EntryList、WaitSet 和 Owner
EntryList 是管程的入口队列,WaitSet 是条件等待队列,Owner 表示当前进入管程的线程
monitorenter 和 monitorexit
synchronized 会被编译成 1 个 monitorenter 和 2 个 monitorexit 命令
monitorenter 进入管程,2 个 monitorexit 分别用于正常和异常退出
synchronized 的线程同步方法
Object o = new Object();
// 进入 o 的 EntryList
synchronized(o) {
// 修改 Owner 指向当前线程
// ...
// 进入 o 的 WaitSet,清空 Owner
o.wait();
// ...
// 随机唤醒 WaitSet 中的一个线程,进入 EntryList
o.notify();
// ...
// 唤醒 WaitSet 中的所有线程,进入 EntryList
o.notifyAll();
}
在有些管程模型中,要求唤醒操作写在代码的最后,这样可以保证通知完其他线程后当前线程刚好结束
在 MESA 模型中,notify 可以写在代码中间,因为 notify 仅仅是将线程放入入口等待队列,并不是立刻执行,而是等待当前进入管程的线程执行完毕才有可能获取进入管程的执行机会
Lock 和 Condition 是 JDK 并发包提供的 API 层面的管程实现
Lock 的实现类有 ReentrantLock、ReentrantReadWriteLock 等
通过 lock.newCondition 方法创建 Condition 实例
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
Condition 有 await、signal、signalAll 方法,功能类似于 sychronized 的 wait、notify、notifyAll
ReentrantLock 比 synchronized 功能更加强大
能够响应中断
void lockInterruptibly() throws InterruptedException;
支持超时
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
非阻塞获取锁
boolean tryLock();
支持公平锁
公平锁可以保证多线程排队获取锁,非公平锁新线程也可能比老线程先获取锁
// 无参构造函数,默认非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
// 根据公平策略参数创建锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
细粒度控制
每个 Condition 都有对应的队列存储等待的线程,使用多个 Condition 使唤醒更具针对性,减少时间损耗
ReentrantLock 实现阻塞队列示例
public class BlockedQueue<T> {
// 容器
private final List<T> list = new ArrayList<>();
// 队列大小
private final int size;
// lock 锁
private final Lock lock = new ReentrantLock();
// 条件变量:队列不满
private final Condition notFull = lock.newCondition();
// 条件变量:队列不空
private final Condition notEmpty = lock.newCondition();
public BlockedQueue(int size) {
this.size = size;
}
public void enqueue(T t) {
lock.lock();
try {
// 队列已满
while (list.size() >= size) {
notFull.await();
}
list.add(t);
//入队后,通知可出队
notEmpty.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public T dequeue() {
lock.lock();
try {
// 队列为空
while (list.size() == 0) {
notEmpty.await();
}
T t = list.remove(0);
//出队后,通知可入队
notFull.signal();
return t;
} catch (InterruptedException e) {
e.printStackTrace();
return null;
} finally {
lock.unlock();
}
}
}
需要注意 try 块里的 while 判断在使用 signal 唤醒时可以用 if 替代
但是在使用 signalAll 时,每次唤醒多个线程,在一个线程执行完成后,后续线程不做判断直接执行,可能会产生异常,此时需要使用 while 循环判断
并发大师 Doug Lea 在《Java 并发编程:设计原则与模式》一书中,推荐了三个用锁的最佳实践
只在更新对象的成员变量时加锁
只在访问可变的成员变量时加锁
不在调用其他对象的方法时加锁