- title: ReentrantLock 公平锁和非公平锁加锁和解锁源码分析(简述)
- date: 2021/8/16
文章目录
一、ReentrantLock
ReentantLock 继承接口 Lock 并实现了接口中定义的方法,他是一种可重入锁,除了能完成
synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。
1. 构造函数
private final Sync sync;
//...
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
无参和有参构造函数,用于构造公平锁或非公平锁。
二、ReentrantLock 公平锁的加锁(lock)过程
参考示例代码:
class ReentrantLockExample {
int a = 0;
ReentrantLock lock = new ReentrantLock();
public void writer() {
lock.lock(); //获取锁
try {
a++;
} finally {
lock.unlock(); //释放锁
}
}
public void reader() {
lock.lock(); //获取锁
try {
int i = a;
//……
} finally {
lock.unlock(); //释放锁
}
}
}
1.1 调用ReentrantLock中的lock
//ReentrantLock.java ReentrantLock
/**
* Acquires the lock.
*
* <p>Acquires the lock if it is not held by another thread and returns
* immediately, setting the lock hold count to one.
*
* <p>If the current thread already holds the lock then the hold
* count is incremented by one and the method returns immediately.
*
* <p>If the lock is held by another thread then the
* current thread becomes disabled for thread scheduling
* purposes and lies dormant until the lock has been acquired,
* at which time the lock hold count is set to one.
*/
/**
* 获取锁。
*
* <p>如果没有被其他线程持有则获取锁并返回
* 立即,将锁定保持计数设置为 1。
*
* <p>如果当前线程已经持有锁,则持有
* count 加一,方法立即返回。
*
* <p>如果锁被另一个线程持有,那么
* 当前线程被禁用以进行线程调度
* 目的并处于休眠状态,直到获得锁,
* 此时锁定保持计数设置为 1。
*/
public void lock() {
sync.lock();
}
1.2 调用Sync中的抽象方法lock,具体实现由子类完成,这里是非公平锁
//ReentrantLock.java Sync
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
/**
* Performs {@link Lock#lock}. The main reason for subclassing
* is to allow fast path for nonfair version.
*/
abstract void lock();
1.3 调用NonfairSync中的 lock;
//ReentrantLock.java NonfairSync
/**
* Sync object for non-fair locks
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
//首先尝试进行CAS操作,将state值从0改为1
if (compareAndSetState(0, 1))
//如果CAS成功,设置exclusiveOwnerThread为当前变量
//获取锁成功说明:1.state从0变为1;(从无竞争资源变为独占资源)
//2. 此锁的exclusiveOwnerThread 为当前线程
setExclusiveOwnerThread(Thread.currentThread());
else
//如果CAS失败,说明有其它的线程在抢占这个锁资源
acquire(1);
}
//AbstractOwnableSynchronizer.java AbstractOwnableSynchronizer
/**
* The current owner of exclusive mode synchronization.
*/
private transient Thread exclusiveOwnerThread;
/**
* Sets the thread that currently owns exclusive access.
* A {@code null} argument indicates that no thread owns access.
* This method does not otherwise impose any synchronization or
* {@code volatile} field accesses.
* @param thread the owner thread
*/
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
//AbstractQueuedSynchronizer.java AbstractQueuedSynchronizer
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
-
首先执行 compareAndSetState(0, 1) ,尝试进行CAS操作,将state值从0改为1;
-
如果CAS成功,调用 setExclusiveOwnerThread(Thread.currentThread()) ,设置exclusiveOwnerThread为当前变量;
获取锁成功说明:
-
state从0变为1;(从无竞争资源变为独占资源) ;
-
此锁的 exclusiveOwnerThread 为当前线程
-
-
如果CAS失败,说明有其它的线程在抢占这个锁资源,执行acquire(1);
-
1.3.1 acquire 方法:
//AbstractQueuedSynchronizer.java AbstractQueuedSynchronizer
/**
* Acquires in exclusive mode, ignoring interrupts. Implemented
* by invoking at least once {@link #tryAcquire},
* returning on success. Otherwise the thread is queued, possibly
* repeatedly blocking and unblocking, invoking {@link
* #tryAcquire} until success. This method can be used
* to implement method {@link Lock#lock}.
*
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquire} but is otherwise uninterpreted and
* can represent anything you like.
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
1.3.1.1 tryAcquire
//ReentranLock.java NonFairSync
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
/**
* Performs non-fair tryLock. tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
*/
final boolean nonfairTryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
int c = getState();
//如果c是0,锁处于空闲状态,使用CAS获取
if (c == 0) {
if (compareAndSetState(0, acquires)) {
//如果CAS成功,设置当前线程是锁的持有者
setExclusiveOwnerThread(current);
return true;
}
}
//如果当前线程是锁的持有者,这是ReentrantLock可重入的原因;
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
//可以看出源码设计者的周到,考虑到了锁溢出的情况
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//将当前线程持有的锁+1
setState(nextc);
return true;
}
return false;
}
1.3.1.2 acquireQueued
1.3.1.2.1 addWaiter()
addWaiter():将当前线程插入至队尾,返回在等待队列中的节点(就是处理了它的前驱后继)。
private Node addWaiter(Node mode) {
//把当前线程封装为node,指定资源访问模式
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
//如果tail不为空,把node插入末尾
if (pred != null) {
node.prev = pred;
//此时可能有其他线程插入,所以使用CAS重新判断tail
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//如果tail为空,说明队列还没有初始化,执行enq()
enq(node);
return node;
}
enq():将节点插入队尾,失败则自旋,直到成功。
private Node enq(final Node node) {
for (;;) {
Node t = tail;
//虽然tail==null才会执行本方法
//但是可能刚好有其他线程插入,会导致
//之前的判断失效,所以重新判断tail是否为空
//队尾为空,说明队列中没有节点
//初始化头尾节点
if (t == null) {
if (compareAndSetHead(new Node()))
//初始化完成后,接着走下一个循环,
//直到node正常插入尾部
tail = head;
} else {
//下面就是链表的正常插入操作了
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
1.3.1.2.2 acquireQueued:
该函数表示将已经在队列中的node(每个线程对应一个node加到等待队列中,具体以后分析)尝试去获取锁否则挂起
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//自旋
final Node p = node.predecessor(); //获得该node
/*
* 如果前置节点是head,表示之前的节点就是正在运行的线程,表示是第一个排队的
* (一般讲队列中第一个是正在处理的,可以想象买票的过程,第一个人是正在买票(处理中),第二个才是真正排队的人);
* 那么再去tryAcquire尝试获取锁,如果获取成功,说明此时前置线程已经运行结束,则将head设置为当前节点返回
*/
if (p == head && tryAcquire(arg)) {
setHead(node);
// help GC,将前置节点移出队列,这样就没有指针指向它,可以被gc回收
p.next = null; // help GC
failed = false;
//返回false表示不能被打断,意思是没有被挂起,也就是获得到了锁
return interrupted;
}
/* shouldParkAfterFailedAcquire将前置node设置为需要被挂起,
*注意这里的waitStatus是针对当前节点来说的,
* 即是前置node的ws指的是下一个节点的状态
*/
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
1.3.1.2.2.1 shouldParkAfterFailedAcquire()
shouldParkAfterFailedAcquire():判断当前节点是否应该被挂起。下面涉及到的等待状态,这里再回忆一下,CANCELLED =1,SIGNAL =-1,CONDITION = -2,PROPAGATE = -3,0
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
//前驱节点的状态是SIGNAL,说明前驱节点释放资源后会通知自己
//此时当前节点可以安全的park(),因此返回true
return true;
if (ws > 0) {
//前驱节点的状态是CANCLLED,说明前置节点已经放弃获取资源了
//此时一直往前找,直到找到最近的一个处于正常等待状态的节点
//并排在它后面,返回false
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//前驱节点的状态是0或PROPGATE,则利用CAS将前置节点的状态置
//为SIGNAL,让它释放资源后通知自己
//如果前置节点刚释放资源,状态就不是SIGNAL了,这时就会失败
// 返回false
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
parkAndCheckInterrupt():若确定有必要park,才会执行此方法。
private final boolean parkAndCheckInterrupt() {
//使用LockSupport,挂起当前线程
LockSupport.park(this);
return Thread.interrupted();
}
1.3.1.3 selfInterrupt
selfInterrupt():对当前线程产生一个中断请求。能走到这个方法,说明acquireQueued()返回true,就进行自我中断。
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
到这里,获取资源的流程就走完了。
##1.4 获取非公平锁总结
在NonSync中的lock函数CAS尝试获取锁;
如果CAS失败,**tryAcquire()**尝试获取资源,该函数是一个钩子函数,在非公平锁中是nonfairTryAcquire();
在nonfairTryAcquire中,会如果当前锁的状态是空闲的,进行CAS尝试获取锁,如果成功返回true,失败返回false; 否则,判断当前线程是否拥有该锁,进行一个可重入的操作,否则返回false;
如果tryAcquire返回false, 调用acquireQueued(addWaiter(Node.EXCLUSIVE), arg)),首先执行的是addWaiter函数;
在addWaiter中,将当前线程插入至队尾(CAS操作),返回在等待队列中的节点(就是处理了它的前驱后继),如果tail为空或CAS失败,执行enq():将节点插入队尾,失败则自旋,直到成功。
然后执行acquireQueued函数,进行一个自旋操作,获取前面的节点,判断是否是头节点head,如果是调用tryAcquire函数再次进行CAS获取锁和可重入的判断,如果成功,然后help GC,将前置节点移出队列,返回interrupted;否则,执行shouldParkAfterFailedAcquire(pred,node),如果返回true,再执行park,如果返回true,interrupted = true,继续自旋;
shouldParkAfterFailedAcquire(pred,node):
首先获取前驱节点的状态,
如果前驱节点的状态是SIGNAL,说明前驱节点释放资源后会通知自己,此时当前节点可以安全的park(),因此返回true;
如果前驱节点的状态是CANCLLED(1),说明前置节点已经放弃获取资源了,此时一直往前找,直到找到最近的一个处于正常等待状态的节点,将当前节点并排在它后面,返回false;
如果前驱节点的状态是0或PROPGATE,则利用CAS将前置节点的状态置为SIGNAL,让它释放资源后通知自己,然后返回false;
parkAndCheckInterrupt():若确定有必要park,才会执行此方法。使用LockSupport,挂起当前线程,并会返回当前线程是否处于中断状态;
如果acquireQueued返回true,执行selfInterrupt():对当前线程产生一个中断请求。能走到这个方法,说明acquireQueued()返回true,就进行自我中断。
就此,加锁过程完成;
三、ReentrantLock 非公平锁和非公平锁的解锁(unlock)过程
二者的解锁是相同的;
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
这个tryRelease也是一个钩子函数:尝试释放锁,彻底释放后返回true
//ReentrantLock.java ReentrantLock
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
//释放锁必须保证当前线程是锁的持有者
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//如果释放后状态值为0,则彻底释放,持有者置空
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
在释放锁的最后写volatile变量 state;
unparkSuccessor():尝试找到下一位继承人,就是确定下一个获取资源的线程,唤醒指定节点的后继节点。
private void unparkSuccessor(Node node) {
//如果状态为负说明是除CANCEL以外的状态,
//尝试在等待信号时清除。
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
//下一个节点为空或CANCELLED状态
//则从队尾往前找,找到正常状态的节点作为之后的继承人
//也就是下一个能拿到资源的节点
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);
}
四、ReentrantLock 公平锁的加锁(lock)过程
//ReentrantLock.java FairLock
final void lock() {
acquire(1);
}
公平锁和非公平锁获取锁只有在钩子函数tryAcquire的实现不同,和非公平锁相比,增加了一个判断当前节点是否是头节点;
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//判断等待队列中是否有前驱节点,没有则尝试获取锁
//hasQueuedPredecessors()返回false,表示没有前驱节点,当前线程就是头节点
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//实现可重入
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
boolean hasQueuedPredecessors():判断当前线程是否位于CLH同步队列中的第一个。如果是则返回flase,否则返回true。
public final boolean hasQueuedPredecessors() {
//判断当前节点在等待队列中是否为头节点的后继节点(头节点不存储数据),
//如果不是,则说明有线程比当前线程更早的请求资源,
//根据公平性,当前线程请求资源失败。
//如果当前节点没有前驱节点的话,才有做后面的逻辑判断的必要性
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
参考连接:
本文详细分析了ReentrantLock的公平锁和非公平锁在加锁与解锁过程中的源码实现。从构造函数开始,通过调用lock()方法进入加锁流程,非公平锁尝试使用CAS快速获取锁,失败则进入队列等待。在解锁时,调用release()方法,尝试释放锁,并唤醒后续等待的线程。公平锁在尝试获取锁时,会检查队列中是否有更早的线程,遵循先来后到原则。关键词包括锁机制、并发控制、AQS、CAS操作和线程同步。

被折叠的 条评论
为什么被折叠?



