ReentrantLock获得锁详细过程
一、 预备知识
ReentrantLock
- ReentrantLock是可重入锁,基于cas实现
- ReentrantLock分为公平锁和非公平锁
二、获取锁的大致过程
- 公平锁:当一个线程在尝试获得锁时,如果锁的state=0,且等待队列为空则获得锁,否则进入队列尾,当持有资源的线程释放锁时唤醒队首线程
- 非公平锁:当一个线程在尝试获得锁时,直接尝试CAS,成功则占有锁,否则进入队列尾,当持有资源的线程释放锁时唤醒队首线程,如果队首线程获得锁成功则弹出,否则不弹出
三、非公平锁详细过程
主要源码
代码前序号和下面解释序号对应
NonfairSync源码:
//1.ReentrantLock获取锁
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
//2.1 非公平锁 尝试获取锁
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
AQS源码:
//2.获取锁+入队列
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//2.2 添加node
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
//2.3 获取+阻塞 死循环
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
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) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
详细过程
1.先执行compareAndSetState(0,1)成功则把当前线程设置为独占线程,成功则获得锁,不成功则继续
2.1 执行acquire(1)(实际调用nonfairTryAcquire(1)),里面会再次看一下state是否为0,是的话compareAndSetState(1)将state设为1并把当前线程设置为独占线程,或者看一下当前独占线程是否是当前线程是的话state+1,返回true结束,当前线程拿到资源,否则返回false
2.2 tryAcquire(1)返回false则需要执行addWaiter(null),这个方法里面是为AQS的双向链表创建了一个node,node的Thread是当前线程,addWaiter()方法里面要更新链表尾结点,因存在多个线程一起抢夺资源的情况,所以调用了compareAndSetTail(),一次不成功的话就会for (;😉 { compareAndSetTail() };这里如果链表为空的话,会new node(),创建一个空的node,让tail和head指向它,所以链表的第一个节点永远是没有实际意义的一个node(参考算法题,此举可能是为了避免tail和head为null带来的麻烦)
2.3acquireQueued()方法中其实就是当前线程争夺锁和阻塞的一个死循环,如果当前线程的node为第二个节点,那就tryAcquire(1),否则就看前一个node的waitStatus,如果waitStatus=SIGNAL的话就表明有事的时候前一个node的线程会唤醒当前线程,当前线程就可以安心的阻塞了,等被唤醒后再去tryAcquire(1),如此往复,直到获取到锁并更新head(head的值为node,但会清空thread和prev属性),这里阻塞是用的LockSupport.park(this)。如果前一个node的waitStatus=CANCELLED的话就表明前一个节点出问题了,就向前找最近的一个waitStatus不为CANCELLED节点,然后再去for循环;如果前一个node的waitStatus=CANCELLED的话就表明前一个节点出问题了,就向前找最近的一个waitStatus不为CANCELLED节点,然后再去for循环;如果前一个node的waitStatus是其他状态的话就将其cas为SIGNAL,然后再去for循环;如果在阻塞过程中出了问题便会中止此线程。
四、公平锁过程
公平锁直接执行第二步,其他相同
final void lock() {
acquire(1);
}
五、优缺点
-
公平锁优缺点:
优点:所有的线程都能得到资源,不会饿死在队列中。
缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
-
非公平锁优缺点:
优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
缺点:可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。