前言
除去使用 synchronized 隐式加锁的方式外,我们可以使用 Lock 更加灵活的控制加锁、解锁、等待和唤醒等操作
Java 中的 Lock 有如下几种实现
- 重入锁(ReentrantLock)
- 读写锁(ReentrantReadWriteLock)
- 等等
不论是重入锁还是读写锁,他们都是通过 AQS(AbstractQueuedSynchronizer)来实现的,并发包作者(Doug Lea)期望 AQS 作为一个构建所或者实现其它自定义同步组件的基础框架
理解 AQS 对理解锁来说至关重要,我们先来利用 AQS 来实现一个自定义的同步组件独占锁
独占锁 Mutex
public class Mutex implements Lock {
private final Sync sync = new Sync();
@Override
public void lock() {
sync.acquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
@Override
public void unlock() {
sync.release(1);
}
@Override
public Condition newCondition() {
return sync.newCondition();
}
private static class Sync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
if (getState() == 0) {
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
Condition newCondition() {
return new ConditionObject();
}
}
}
复制代码
整体调用逻辑如下
加锁
调用 lock() 的时候,首先会去调用 acquire() 方法
解锁 unlock()
解锁过程
以上对 AQS 的部分功能接口做了讲解,后面结合其它锁会逐步展开剩下的点
重入锁 ReentrantLock
ReentrantLock 有两种实现分别是
- 公平锁
- 非公平锁
公平锁
在创建 ReentrantLock(true) 就可以创建公平锁的实现,在其内部指定了内部的 AQS 使用 FairSync
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
复制代码
获取锁逻辑
释放锁逻辑
非公平锁
非公平锁比公平锁的实现简单了许多而且性能也好了很多,区别主要在于
- 加锁的时候非公平锁直接去设置同步状态,而公平锁必须是头部结点
其它基本都是相似的
我们知道 AQS 是一个 FIFO 队列,从中唤醒的都是有先后顺序的,同时在这种情况下又多了操作就是由于线程是可以重入的,那么在这种情况下,同一个线程可能会连续的持有锁,导致处于等待中的队列等待更久,比如如下代码每个线程获取锁 2 次,启动了多个任务
读写锁
读写锁用一个值维护读和写锁,用高 16 位表示读状态,用低 16 位表示写状态
读锁
- 第一处,获取写锁状态如果不为 0,再次判断是否是当前线程,如果不是则返回获取失败
- 第二,三处,获取读锁的值,并且增加重入次数,并且统计每个线程锁的重入次数
写锁
- 第一处,如果 state 不为 0 表示锁对象被线程持有
- 第二处,只要存在读锁返回获取锁失败放入同步队列中,如果写线程数量超过了最大值抛出异常
- 第三处,设置重入次数
- 第四处,如果 state 为 0 表示初次获取锁,CAS 抢锁,成功则返回失败则放入同步队列中
- 第五处,设置锁对象被当前线程持有
总结一下:
- 读锁和写锁都是可重入锁
- 可以支持锁降级
- 如果一个线程持有了写锁则可以再次持有读锁然后释放写锁
- 如果一个线程持有了读锁,则不能当前线程和其它线程都不能再持有写锁
Condition
一个锁可以持有一个同步队列,多个 Condition,每个 Condition 上存在多个等待结点
当调用 await() 的时候就会构造对应的结点,首先从同步队列头部移除结点,然后释放锁,然后放入 Condition 列表尾部
- 第一处,将当前节点构造成 waiter 节点放入 Condition 等待队列尾部
- 第二处,释放当前线程同步状态,然后唤醒同步队列后继头结点
- 第三处,当前线程挂起等待被唤醒或者中断返回
当调用 singal() 的时候就会从 Condition 等待队列中移除放入同步队列的尾部
- 第一处,判断必须释放当前线程持有的锁
- 第二处,唤醒 Condition 第一个等待的结点,将其断开加入到同步队列的尾部,并且调用 unpark 将其唤醒
参考:
- 并发编程的艺术