大家好,牧码心今天给大家推荐一篇并发编程系列(八)—深入理解基于AQS的ReentrantLock的文章,希望对你有所帮助。内容如下:
ReentrantLock是一种可重入的独占锁,实现了Lock接口,依赖于AQS实现的同步锁机制,具有synchronized基本相同的行为,但也扩展了更多功能,如可中断,非公平和公平锁等,为了帮助大家更好地理解ReentrantLock的特性,我们先将ReentrantLock跟Synchronized进行比较:
维度 | synchronized | ReentrantLock |
---|---|---|
锁的类型 | 可重入,非公平 | 可重入,非公平,公平 |
锁的状态 | 不可中断 | 可中断 |
锁的释放 | JVM自动释放 | 通过unlock()显示释放 |
锁的获取 | JVM隐式获取 | Lock()获取锁 |
锁的实现机制 | 通过JVM监视器实现 | 通过AQS实现 |
从性能方面上来说,在并发量不高、竞争不激烈的情况下,Synchronized 同步锁由于具有分级锁的优势,性能上与 Lock 锁差不多;但在高负载、高并发的情况下,Synchronized 同步锁由于竞争激烈会升级到重量级锁,性能则没有 Lock 锁稳定。
ReentrantLock 实现了Lock接口,通过构造器提供了指定公平策略 / 非公平策略的功能,默认为非公平策略,主要函数说明如下:
主要函数
构造函数
// 创建一个 ReentrantLock ,默认是“非公平锁”。
ReentrantLock()
// 创建策略是fair的 ReentrantLock。fair为true表示是公平锁,fair为false表示是非公平锁。
ReentrantLock(boolean fair)
获取锁,判断锁,释放锁等函数
// 返回目前拥有此锁的线程,如果此锁不被任何线程拥有,则返回 null。
protected Thread getOwner()
// 如果是“公平锁”返回true,否则返回false。
boolean isFair()
// 查询此锁是否由任意线程保持。
boolean isLocked()
// 获取锁。
void lock()
// 如果当前线程未被中断,则获取锁。
void lockInterruptibly()
// 返回用来与此 Lock 实例一起使用的 Condition 实例。
Condition newCondition()
// 仅在调用时锁未被另一个线程保持的情况下,才获取该锁。
boolean tryLock()
// 如果锁在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁。
boolean tryLock(long timeout, TimeUnit unit)
// 试图释放此锁。
void unlock()
ReentrantLock 典型使用方式如下:
public void test () throw Exception {
// 1.初始化选择公平锁、非公平锁
ReentrantLock lock = new ReentrantLock(true);
// 2.可用于代码块
lock.lock();
try {
try {
// 3.支持多种加锁方式,比较灵活; 具有可重入特性
if(lock.tryLock(100, TimeUnit.MILLISECONDS)){ }
} finally {
// 4.手动释放锁
lock.unlock()
}
} finally {
lock.unlock();
}
}
我们从ReentrantLock的类结构中可以看到,ReentrantLock里面有一个内部类Sync,Sync继承AQS(AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实际上都是在Sync中实现的。它有公平锁FairSync和非公平锁NonfairSync两个子类。ReentrantLock默认使用非公平锁,也可以通过构造器来显示的指定使用公平锁。如图所示:
从图中我们可以看到ReentrantLock支持公平锁和非公平锁,并且ReentrantLock的底层就是由AQS来实现的。那么ReentrantLock是如何通过公平锁和非公平锁与AQS关联起来呢?下面我们分别根据这2中锁的加锁和释放锁流程来分析实现原理。
我们先假设一个场景,如下:
假设现在有几个线程T1,T2,同时去竞争同一个共享资源,在公平锁机制下又是一个怎样的流程呢?我们从线程T1获取锁开始,ReentrantLock lock = new ReentrantLock(true)
public void lock() {
sync.lock();
}
其实是调用了FairSync的lock方法:
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
而FairSync的lock方法有调用了acquire(1); acquire()方法是AQS中定义的方法:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
我们可以看到核心实现是来自tryAcquire和acquireQueued这2个方法。
protected final boolean tryAcquire(int acquires) {
//1.得到当前线程
final Thread current = Thread.currentThread();
// 2.获取独占锁的同步状态state
int c = getState();
// 3.若state的值为0,表示锁未占用,则
if (c == 0) {
// 3.1 若等待队列CLH中,当前线程前有没有等待更久的线程,若没有则以CAS更新同步状态为1
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
// 3.2 更新成功,设置当前线程拥有独占锁
setExclusiveOwnerThread(current);
return true;
}
}
// 4判断是否属于重入情况,重入时同步状态累加+1
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
// 4.1超出次数,则溢出
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// 4.2设置同步状态
setState(nextc);
return true;
}
return false;
}
线程T1获取锁成功后,接着线程T2来竞争锁,此时调用tryAcquire()方法获取锁失败,则会调用addWaiter()方法。
private Node addWaiter(Node mode) {
// 1.创建当前线程对应的Node节点,并设置锁的模型是mode
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
// 2.若CLH队列不为空,则将当前线程的节点添加到队尾
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 3.如果CLH队列为空,则新建一个CLH表头;然后将node节点添加到CLH末尾。否则,直接将node节点添加到CLH末尾。
enq(node);
return node;
}
线程T2被包装成等待获取锁的节点插入到CLH队尾后,接着在调用acquireQueued()方法。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
// 1.interrupted标识在CLH队列的调度中,当前线程进入阻塞时,有没有被中断过。
boolean interrupted = false;
// 2.从等待队列CLH的选取队首节点的线程,并尝试获取锁,若获取不到,则会进入阻塞状态
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 判断当前线程有没有被阻塞
if (shouldParkAfterFailedAcquire(p, node) &&
// 阻塞当前线程,并且返回线程被唤醒之后的中断状态。通过LockSupport.park()阻塞“当前线程”,然后通过Thread.interrupted()返回线程的中断状态
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 1.得到前驱节点状态
int ws = pred.waitStatus;
// 2.如果前驱节点是SIGNAL状态,则意味这当前线程需要被unpark唤醒。此时,返回true。
if (ws == Node.SIGNAL)
return true;
// 3.若前驱节点是取消状态,则设置当前线程的节点为前驱节点
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 4.若前驱节点是0或共享锁的状态,则需要设置前驱节点为SIGNAL状态。
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
关于节点状态说明:
节点状态 | 值 | 说明 |
---|---|---|
SIGNAL | -1 | 发信号,表示当前线程的后继线程需要被unpark(唤醒)。一般发生情况是:当前线程的后继线程处于阻塞状态,而当前线程被release或cancel掉,因此需要唤醒当前线程的后继线程 |
CANCELLED | 1 | 取消。表示后驱结点被中断或超时,需要移出队列 |
CONDITION | -2 | 条件,表示当前线程在等待Condition唤醒 |
PROPAGATE | -3 | 共享,表示其它线程获取到共享锁 |
至此公平锁加锁的流程分析结束,现在总结下整个加锁过程:
1.线程T1调用acquire()方法获取锁,会先通过tryAcquire()尝试获取锁。获取成功的话,直接返回;尝试失败的话,再通过acquireQueued()获取锁;
2.若第一步线程T1获取成功,由于公平性原则,线程T2则会获取失败,则会先通过addWaiter()来将当前线程加入到CLH队列末尾;然后调用acquireQueued(),在CLH队列中排序等待获取锁,在此过程中,线程T2处于休眠状态,直到获取锁了才返回;
3.如果线程T2在休眠等待过程中被中断过,则调用selfInterrupt()来自己产生一个中断。
依据公平性原则,线程T1用完后将调用unlock()方法来释放锁,实现逻辑如下:
public void unlock() {
sync.release(1);
}
可以看到unlock()内部调用的是AQS的release方法,传参为1。
注意:“1”的含义和“获取锁的函数acquire(1)的含义”一样,它是设置“释放锁的状态”的参数。由于“公平锁”是可重入的,所以对于同一个线程,每释放锁一次,锁的状态-1。
protected final boolean tryRelease(int releases) {
// 1.同步状态值-1
int c = getState() - releases;
//2.判断持有锁线程和释放锁线程是不是同一个,否则抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 若C=0 说明没有线程占用锁,并清除占用线程
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
// 更新状态值
setState(c);
return free;
}
线程T1释放成功后,调用unparkSuccessor方法,唤醒队列中的首结点。
private void unparkSuccessor(Node node) {
// 1.获取当前线程的状态
int ws = node.waitStatus;
// 2.如果状态<0,则通过CAS设置状态=0
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
// 获取当前节点的有效的后继节点,无效的话,则通过for循环继续获取。当后继节点对应的线程状态<=0为有效。
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 若后继线程不为空,则唤醒对应的线程
if (s != null)
LockSupport.unpark(s.thread);
}
队首节点线程T2被唤醒后,获得独占锁资源,继续执行完并也调用unlock方法释放锁。
ReenrantLock非公平锁的内部实现中释放锁和公平锁的实现一样,不同的地方是在获取锁。非公平锁和公平锁在获取锁的主要区别在于:
1.公平锁会判断CLH等待队列中是否有线程排在当前线程前面。只有没有情况下,才去获取锁。
2.非公平锁是只要当前锁处于空闲状态,则直接获取锁,而不管在CLH等待队列中的顺序。只有当非公平锁尝试获取锁失败的时候,它才会像公平锁一样,进入CLH等待队列排序等待。实现逻辑如下:
final void lock() {
// 1.先尝试修改同步状态,若获取失败后再调用AQS的acquire方法
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
acquire方法会调用非公平锁自身的tryAcquire方法,最终是调了nofairTryAcquire方法,而该方法相对于公平锁,只是少了“队列中是否有其它线程排在当前线程前”这一判断:
最后用一个逻辑流程图来总结ReetrantLock 获取锁的过程,如图所示: