ReentrantLock
可重入锁,即线程可以多次对资源进行加锁操作,线程在获取到锁后可以再次获取,不会被锁阻塞。
支持非公平和公平
非公平锁:修改state为0到1,如果成功,说明获取了锁资源,失败则进入AQS双向队列进行等待(调用acquire)
Lock()
公平锁:调用acquire
什么是state?
state其实是AQS中一个由volatile修饰的int变量,多个线程会通过CAS去修改state值,而在并发情况下只有一个线程修改成功,剩下修改失败的线程进入等待
AQS(AbstractQueuedSynchronizer)双向链表?
通过查看源码可以发现AQS基于Node内部类进行维护,Node包含有pre,thread,next,还有head和tail
既然非公平锁和公平锁都调用了acquire方法,我们来看看这个方法
acquire是一个业务方法,没有实际业务处理,调用了其他方法
先看看tryAcquire
tryAcquire主要做了两件事:
- 如果state为0,尝试获取锁资源
- 如果state不为0,看一下是不是锁重入操作
公平锁方法:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 拿到AQS的state值,进行判断
int c = getState();
if (c == 0) {
// 判断是否有线程在排队,如果有线程排队,则无法获取到所资源,直接返回最外层的false
if (!hasQueuedPredecessors() &&
// 如果没有线程排队,直接CAS尝试获取锁资源
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 当前state != 0,说明有线程占用着锁资源
// 判断当前占用线程和尝试获取锁资源的线程是否相同,如果相同,则说明要进行锁重入操作
else if (current == getExclusiveOwnerThread()) {
// 将state再次+1
int nextc = c + acquires;
// 判断锁重入是否超过最大限制
// 01111111 11111111 11111111 11111111 + 1
// 10000000 00000000 00000000 00000000
// 抛出error
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// 否则修改锁重入后的state值
setState(nextc);
return true;
}
return false;
}
非公平锁:
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;
}
再来看看addWaiter方法的实现
// 获取锁资源失败后,将当前线程封装为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放置在tail后,以下就是链表插入的操作了
Node pred = tail;
// 如果pred不为null,有线程正在排队
if (pred != null) {
// 将当前节点的prev,指定tail尾节点
node.prev = pred;
// 以CAS的方式,将当前节点变为tail节点
if (compareAndSetTail(pred, node)) {
// 之前的tail的next指向当前节点
pred.next = node;
return node;
}
}
// 如果cas插入节点失败,直接使用enq()方法去插入节点
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
// 拿到tail
Node t = tail;
// 如果tail为null,说明当前没有Node在队列中
if (t == null) { // Must initialize
// 创建一个新的Node作为head,并且将tail和head指向一个Node
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 插入节点操作
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
回到acquireQueued方法
acquireQueued方法会查看当前排队的Node是否是head的next,如果是,尝试获取锁资源,如果不是或者获取锁资源失败那么就尝试将当前Node的线程挂起(unsafe.park())
在挂起线程前,需要确认当前节点的上一个节点的状态必须是小于等于0,
如果为1,代表是取消的节点,不能挂起
如果为-1,代表挂起当前线程
如果为-2,-3,需要将状态改为-1之后,才能挂起当前线程
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 拿到上一个节点
final Node p = node.predecessor();
if (p == head // 当前排队的Node为head.next
&& tryAcquire(arg) // 尝试获取锁资源,成功返回true 失败返回false
) {
// 将当前节点置为head,thread和prev属性置位null
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 如果不是或者获取锁资源失败,尝试将线程挂起
if (shouldParkAfterFailedAcquire(p, node) &&
// 通过LockSupport将当前线程挂起
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 上一个节点状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL) //节点状态为-1 可挂起
return true;
// 如果上一个节点是取消状态
if (ws > 0) {
// 循环往前找,找到一个状态小于等于0的节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 如果为-2,-3,需要将状态改为-1之后,才能挂起当前线程
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
以上是进行lock方法的源码介绍,接下来谈谈锁资源的释放
UnLock()
释放锁资源:
- 将state-1。
- 如果state减为0了,唤醒在队列中排队的Node。(一定唤醒离head最近的)
释放锁不分公平和非公平,就一个方法
// 真正释放锁资源的方法
public final boolean release(int arg) {
// 核心的释放锁资源方法
if (tryRelease(arg)) {
// 释放锁资源释放干净了 (state == 0)
Node h = head;
// 唤醒新的节点
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
// 核心的释放锁资源方法
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
// 如果释放锁的线程不是占用锁的线程,抛异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// 是否成功的将锁资源释放利索 (state == 0)
boolean free = false;
if (c == 0) {
// 锁资源释放干净。
free = true;
// 将占用锁资源的属性设置为null
setExclusiveOwnerThread(null);
}
// 将state赋值
setState(c);
return free;
}
// 唤醒节点
private void unparkSuccessor(Node node) {
// 拿到头节点状态
int ws = node.waitStatus;
// 如果头节点状态小于0,换为0
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 拿到当前节点的next
Node s = node.next;
// 如果s == null ,或者s的状态为1
if (s == null || s.waitStatus > 0) {
// next节点不需要唤醒,需要唤醒next的next
s = null;
// 从尾部往前找,找到状态正常的节点。(小于等于0代表正常状态)
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 经过循环的获取,如果拿到状态正常的节点,并且不为null
if (s != null)
// 唤醒线程
LockSupport.unpark(s.thread);
}
为什么唤醒线程时,为啥从尾部往前找,而不是从前往后找?
因为在addWaiter操作时,是先将当前Node的prev指针指向前面的节点,然后是将tail赋值给当前Node,最后才是上一个节点的next指针,指向当前Node。
如果从前往后,通过next去找,可能会丢失某个节点,导致这个节点不会被唤醒~
如果从后往前找,肯定可以找到全部的节点。