开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第6天,点击查看活动详情
相信对并发有所了解的同学对于ReentrantLock并不陌生。没错,今天的主角就是可重入锁ReentrantLock
非公平锁
ReentrantLock的无参构造默认就会创建一个非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
我们来看看他是如何加锁的
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
- 首先是通过CAS设置state值为1;
- 哪个线程设置成功了,就将此线程设置成独占锁线程,也就是此线程获得锁;
- 否则执行acquire(1)。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
我们先分析tryAcquire(arg)和acquireQueued(addWaiter(Node.EXCLUSIVE), arg)子方法,最后在回过头来分析acquire(int arg)方法
先看tryAcquire(arg)方法
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
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;
}
- 首先获取
state的值,判断是否为0,也就是判断是否有线程占用锁; - 如果没有线程占用锁,当前线程通过CAS尝试设置
state值(尝试获取锁),如果成功返回true; - 如果当前线程和独占锁的线程是同一个线程,则将
state增加acquires(可重入),并返回true; - 否则返回false。
总结下tryAcquire(arg)方法的作用:尝试获取锁,获取到了锁返回true,否则返回false。
addWaiter(Node.EXCLUSIVE)方法主要是做了一个线程入队的操作,这里就不细讲;
主要来看看acquireQueued(addWaiter(Node.EXCLUSIVE), arg)这个方法。
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);
}
}
可以看到这个方法的主体内容就是一个死循环。
- 首先获取当前线程的node节点的上个节点
- 上个节点如果是头节点,尝试获取锁;
- 如果获取到锁,设置当前节点为头节点,并将之前的头节点出队;
- 如果上述判断有一个为false,执行
shouldParkAfterFailedAcquire(p, node)方法 - 如果
shouldParkAfterFailedAcquire方法返回true,则调用park方法堵塞当前线程; - 如果返回false,则继续循环。
来看看shouldParkAfterFailedAcquire(p, node)到底做了什么
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
- 判断上个节点的
waitStatus值 - 如果=-1,返回true;
- 如果>0,则死循环去找到上上个节点(直到
waitStatus<=0为止),返回false; - 否则将上个节点的
waitStatus设置为-1,返回false。
实际上,shouldParkAfterFailedAcquire(p, node)方法就是判断是否需要堵塞当前线程,并修改上个节点的waitStatus的值为-1,表示上个节点的线程处于休眠状态。
最后,我们来看看真正堵塞的方法parkAndCheckInterrupt()
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
就是调用park方法进行线程的堵塞,也就是说线程执行到这就会释放CPU,停在了这,等待被唤醒。
公平锁
公平锁绝大地方跟非公平锁的实现没啥区别,无非是在CAS获取锁之前判断了一下等待队列中的线程是否轮到了当前线程(hasQueuedPredecessors()方法)
总结
到此,ReentrantLock加锁过程就聊完了,这里简单做个总结。
- 首先通过CAS设置
state= 1成功就是获取到锁; - 失败则再次尝试获取锁;
- 如果还是失败,则将这个线程节点加入等待队列;
- 如果这个节点的上个节点是头节点会再次尝试获取锁,如果成功即获取锁,并进行节点链表的新旧头节点的出入队列操作;
- 如果失败则会调用park进行堵塞,等待被唤醒。
文中如有不足之处,欢迎指正!一起交流,一起学习,一起成长 ^v^