如果 把ReentrantLock比做一个人的话,那么 AQS 就是他的灵魂。离开 AQS 谈论锁都是耍流氓
ReentrantLock and AQS
一.AQS使用方式和其中的设计模式
继承,模板方法设计模式
二.重要参数
- private volatile int state; 记录当前锁是否有线程拿到锁、一个线程进入锁的重入数。如果是0,代表没有任何线程进入锁。如果是 n,n>0 那么代表有个线程重入了 n 次
- AbstractOwnableSynchronizer: private transient Thread exclusiveOwnerThread:当前拿锁的线程
- volatile int waitStatus:竞争失败的线程会打包成Node放到同步队列,Node可能的状态里:
- CANCELLED(cancelled 1):该节点的线程可能由于超时或被中断而处于被取消(作废)状态,一旦处于这个状态,节点状态将一直处于CANCELLED(作废),因此应该从队列中移除.
- SIGNAL(signal -1):拿到锁的节点(head)和未拿到锁正常等待的节点 waitState 都是 signal 。当前节点为SIGNAL时,后继节点会被挂起,因此在当前节点释放锁或被取消之后必须被唤醒(unparking)其后继结点. 如:如果head 释放锁以后,就会判断 head.next 是否是 signal,是的话唤醒。
- CONDITION(condition -2) :当前节点处于等待队列
- PROPAGATE(propagate -3):共享,表示状态要往后面的节点传播
0,表示初始状态(拿完锁的状态)
- private transient volatile Node head:等待队列的头
- private transient volatile Node tail:等待队列的尾
三.了解其中的方法
1.模板方法:
独占式获取
accquire
acquireInterruptibly
tryAcquireNanos
共享式获取
acquireShared
acquireSharedInterruptibly
tryAcquireSharedNanos
独占式释放锁
release
共享式释放锁
releaseShared
2.需要子类覆盖的流程方法
独占式获取 tryAcquire
独占式释放 tryRelease
共享式获取 tryAcquireShared
共享式释放 tryReleaseShared
这个同步器是否处于独占模式 isHeldExclusively
3.同步状态state:
getState:获取当前的同步状态
setState:设置当前同步状态
compareAndSetState 使用CAS设置状态,保证状态设置的原子性
三、源码
1.lock
实现类源码:ReentrantLock 为例
final void lock() {
acquire(1);
}
AbstractQueuedSynchronizer 源码
//p1197
public final void acquire(int arg) {
//tryAcquire 为需要覆盖,需要子类实现的方法
//EXCLUSIVE表示正在一个独占模式下等待
//如果一个线程执行 tryAcquire 恰好拿到了锁,那么就不再执行acquireQueued了。也不会放到待执行的队列了
if (!tryAcquire(arg) &&
//addWaiter 下面 p605
//acquireQueued 下面857
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//子类实现tryAcquire 方法示例
protected boolean tryAcquire(int arg) {
if(compareAndSetState(0,1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
如下为 addWaiter 方法 这一支的操作。
把调用 lock 的线程包装成 node 节点放到队列中,并把这个 node 节点返回
判断当前队列有没有尾节点(队列是否为空):
- 如果不为空那么就把当前节点挂在队列末尾(进行双链表的一些操作),设置当前节点为尾节点
- 如果为空,调用 enq 的方法,构建一个『空节点node ⇆ 当前线程 node』这样的双链表,enq 中使用自旋 CAS 来添加,防止有其他线程更改
//p605
//mode == null
private Node addWaiter(Node mode) {
// 使用当前线程 构造一个 node,构造器如下 p505
Node node = new Node(Thread.currentThread(), mode);
//如果当前有尾节点(等待队列不为空,非初始化)
Node pred = tail;
if (pred != null) {
// 当前节点的前驱节点地址赋值
node.prev = pred;
// 使用 CAS 把当添加进来的节点设置成尾节点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 如果当前没有尾节点(等待队列为空,初始化队列的时候) p583
enq(node);
return node;
}
//p505
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
如果尾节点为空(那么head也是空的,整个队列都是空的),才会执行enq。
所以此方法最后会形成一个『空节点node ⇆ 当前线程 node』,这样一个双链表
//p583
private Node enq(final Node node) {
//自旋
for (;;) {
Node t = tail;
//队列初始化
if (t == null) {
//cas 方式设置 空节点 到 head 中,且 tail 与 head 同步
if (compareAndSetHead(new Node()))
tail = head;
} else {
//当前节点 的 上一个节点指针指向 if 中 造出来的那个 空节点
node.prev = t;
//cas 设置当前节点为尾节点
if (compareAndSetTail(t, node)) {
//if 中 造出来的那个空节点的下一个节点指向当前节点
t.next = node;
return t;
}
}
}
}
如下为acquireQueued 的操作
head 节点为已经拿到锁的线程
此方法基本思路
1.先拿到当前节点的上一个节点,如果上一个节点为head且当前节点尝试拿了一下锁拿到了。那么就把当前节点设置成head,上一个节点脱钩,返回 false
2.否则就会执行shouldParkAfterFailedAcquire、parkAndCheckInterrupt
3.感觉如果p == head && tryAcquire(arg)条件不满足循环将永远无法结束,当然不会出现死循环,奥秘在于parkAndCheckInterrupt会把当前线程挂起,从而阻塞住线程的调用栈(阻塞住的意思就是走到这个方法parkAndCheckInterrupt,就不动了,卡住了)。
//p857
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
//如果他的前驱节点是头结点,且已经执行完毕(state 已经复位),执行tryAcquire后被我当前线程抢占了锁,那么执行把当前节点设置为head的操作
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//下面的两个方法主要是检查线程的状态,是否被中断。且阻塞当前线程(parkAndCheck)
//shouldParkAfterFailedAcquire p795
//parkAndCheckInterrupt p835
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
//如果发生异常,会执行此段代码,取消加入队列
cancelAcquire(node);
}
}
#p795
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.
//规则1:该方法首先检查前趋结点的waitStatus位,如果为SIGNAL,表示前趋结点会通知它,那么它可以放心大胆地挂起了,返回 true 后调用acquireQueued方法的 parkAndCheckInterrupt)将导致线程阻塞
return true;
if (ws > 0) {
//Predecessor was cancelled. Skip over predecessors and indicate retry.
//规则2:如果前趋结点是一个被取消的结点怎么办呢?那么就向前遍历跳过被取消的结点,直到找到一个没有被取消的结点为止,将找到的这个结点作为它的前趋结点,将找到的这个结点的waitStatus位设置为SIGNAL,返回false表示线程不应该被挂起。然后进去acquireQueued方法循环
//接下来acquireQueued循环会出现两种结果
//1.因为在这一步干掉了前一个被取消了的节点,把前前一个节点变成了前一个节点,恰巧前一个节点还是头结点,所以当前节点尝试tryAcquire可能会获取到锁。
//2.前前一个节点不是头结点,那么就会再次进入到该方法。此次前一个节点肯定是SIGNAL状态,所以当前节点被毫无疑问的挂起(阻塞)
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.
*/
//如果前继节点状态为非SIGNAL、非CANCELLED,则设置前继的状态为SIGNAL,返回false后进入acquireQueued的无限循环
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
p835
//这个方法的主要任务就是阻断线程
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
总结
- 子类实现 tryAcquire方法来具体操作拿锁的操作。
- 用户调用acquire 方法,acquire 首先调用tryAcquire看看是否能拿到锁。如果拿到了那么返回 true,程序也能顺利往下执行,同时业务代码也会被执行
- 如果拿不到,那么就把当前线程封装成一个 Node,通过调用addWaiter放到一个双链表当中。
- addWaiter还会把这个node 返回,在acquireQueued方法会进行自旋,如果当前节点的前一个节点为头结点,本节点会再一次调用tryAcquire尝试进行拿锁的操作。
- 如果拿不到接着执行,在shouldParkAfterFailedAcquire 中把前一个节点的 waitStatus 由默认的 0 改成 SIGNAL(-1),当前节点作为尾节点waitStatus状态还是 0。
- 改完waitStatus状态之后,执行parkAndCheckInterrupt 阻塞当前线程。注:如果shouldParkAfterFailedAcquire返回false,&& 会短路,parkAndCheckInterrupt 阻塞当前线程。注:如果shouldParkAfterFailedAcquire就不执行了。但是acquireQueued是一个自旋,待执行shouldParkAfterFailedAcquire,waitStatus状态都改成-1后,还是会执行 parkAndCheckInterrupt来阻塞当前线程
2.unlock
public final boolean release(int arg) {
//tryRelease aqs 的具体实现类重写
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
//设置 waitstatus 为0
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
//如果下个节点是null或者下个节点被cancelled,就找到队列最开始的非cancelled的节点
if (s == null || s.waitStatus > 0) {
s = null;
//为什么要从后往前找??原因在addWaiter方法:
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null){
//释放 一个被阻塞的线程
LockSupport.unpark(s.thread);
}
}
//我们从这里可以看到,节点入队并不是原子操作,也就是说,node.prev = pred; compareAndSetTail(pred, node) 这两个地方可以看作Tail入队的原子操作
//但是此时pred.next = node;还没执行,如果这个时候执行了unparkSuccessor方法,就没办法从前往后找了,所以需要从后往前找。
//还有一点原因,在产生CANCELLED状态节点的时候,先断开的是Next指针,Prev指针并未断开,因此也是必须要从后往前遍历才能够遍历完全部的Node。
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
调用 LockSupport.unpark(s.thread); 释放当前线程阻塞,程序顺序往下执行业务代码。
四、ReentrantLock源码分析
1.构造器
参数为空或 false 是非公平锁。为 true 是公平锁。
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
2. 非公平锁获取锁
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
//如果直接修改了 state ,那么就证明现在没有其他资源在获得锁,本线程直接拿到锁。(原来锁就是操作了 一个 volitale 的 state)
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
//实现 aqs 的 tryAcquire
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//如果是0,证明还没有线程拿到这个锁,直接 cas 设置 state
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;
}
//如果当前锁已经被获取,且拿锁的线程不是当前线程,那么返回 false,tryAcquire 失败
return false;
}
3.公平锁获取锁
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//如果目前没有线程拿锁
if (c == 0) {
//hasQueuedPredecessors 是 公平锁的关键
//对下面的 if 换一种好理解的写法
/**
if(!hasQueuedPredecessors()){
if(compareAndSetState(0, acquires)){
setExclusiveOwnerThread(current);
return true;
}
}
*/
//如果 hasQueuedPredecessors 返回 false,那么证明等待队列中没有线程在排队。 那么执行 compareAndSetState 拿锁
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;
}
}
//hasQueuedPredecessors是公平锁加锁时判断等待队列中是否存在有效节点的方法。
//如果返回False,说明当前线程可以争取共享资源;如果返回True,说明队列中存在有效节点,当前线程必须加入到等待队列中。
//判断队列中是否有 有效节点,以真实线程节点 入队为准。不以虚节点(new Node())为准
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
//head 的下一个元素
Node s;
//1.如果 h != t 为false (h == t 为 true) ,那么直接返回 false,在tryAcquire 中执行 set state 操作
//2.(s = h.next) == null && h != t 是走到了 enq ①处,此时 tail == node,head == new Node()。
// head 的 next 还未指向 tail,但是 node 已入 tail。所以公平起见当前线程就不能再抢占锁了。 返回 true,使当前线程不会再设置 state
//3.如果第二次拿锁的线程 和第一次拿线程的线程不是同一个线程,那么第二个线程应该让渡第一个线程(如果是同一个线程,那么两次拿锁就不分先后)
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
//分析此段代码需从 enq 方法看起
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) {
// Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
//①
t.next = node;
return t;
}
}
}
}
总结:
- NonfairSync 和 fairSync 的根本区别在于tryAcquire 中 当 state == 0 时(即当前锁没有被任何一个线程占有),当前线程是直接尝试拿锁(非公平)还是查看等待队列是否有元素来决定去排队还是尝试拿锁(当等待队列有元素就排队,没有元素就拿锁)。
- 公平锁直接的提现 方法是 hasQueuedPredecessors
- tryAcquire 有两个地方调用。1)lock 的时候直接调用一次。2)加入队列后再次尝试调用。
final void lock() {
acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
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);
}
}
4.释放锁
protected final boolean tryRelease(int releases) {
//可重入锁 state -1
int c = getState() - releases;
//如果解锁线程 和 持锁线程不是同一个 抛异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//如果当前线程的锁全部解完,那么就代表 此线程已经释放了锁
if (c == 0) {
//释放锁成功
free = true;
//持锁线程 释放
setExclusiveOwnerThread(null);
}
//设置 state 数量
setState(c);
return free;
}