两种获取锁方法
- 尝试获取锁,失败则返回。这种获取方式不会加入队列进行自旋操作
- 一定要获取锁。这种操作一旦获取失败,就会加入等待队列等待锁资源
这里重点说一下第二种方法
提示:以下是本篇文章正文内容,下面案例可供参考
原理
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire即第一种方法,尝试获取锁,当尝试获取锁失败并且后面的方法返回true的时候,进行selfInterrupt。
这么现在来一步一步拆解着看
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;
}
这里首先将当前加入的线程封装成一个新的节点,其中tail为当前等待队列的尾节点,如果尾节点不为空,则通过CAS操作将尾节点设置为当前节点的前置节点,并将尾节点设为当前节点的前置节点。这时候当前节点成为尾节点,并返回当前节点。
这里的compareAndSetTail为unsafe中的一个native的方法,提供原子操作,所以这里将当前节点置为尾节点的操作是线程安全的。
那么if里面的代码块会有线程安全的问题嘛?并不会,因为if里面的代码块只是将前置节点的指针指向了当前节点。
这是一个快速入队列的方法,如果失败了(即第一次自旋失败或者尾节点为空),则调用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;
}
}
}
}
这里首先判断尾节点是否为空,如果是空的说明只有一个头结点。
注意,AQS使用了哨兵模式,头结点是一个哨兵结点,为什么要有这个节点呢?是为了防止只剩一个节点时,head指针和last指针同时指向该节点且同时发生出队列和入队列的情况下,出现空指针的问题。进队列调用last.next=cur,但是此刻发生出队列,那么last为空,自然就空指针异常了。这是线程不安全的。因此将头节点设为哨兵节点,既有监控之后节点状态的目的,也有线程安全的目的。
好了,继续源码分析。
上面说到如果尾节点为空,则将尾节点设为头节点,方便下一个节点入队。
如果尾节点不为空,就更换当前节点。之后的操作就和快速入队一样了。
那么为什么要分完全入队和快速入队呢?因为完全入队多了判空的操作。分离之后可以少判一次空?这是我的想法。有大佬说Java 9已经实现合并了,本人用的Java 8 还不太清楚。不知道有没有大佬告知一下。
接下来入队列操作就结束了,那么节点是如何自旋或者如何获取锁的呢?
接下来看看acquireQueued方法
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的waitStatus置为CANCELLED状态。
关于获取锁的自旋,则判断当前节点的前置节点是否为头节点且当前节点获取锁成功了。如果有一个条件不满足,就会被挂起。
这里又有一个interrupted 的变量,分析代码可以看出,当线程需要被挂起并且成功挂起,interrupted 会被设置为true。是否需要被挂起则需要去判断节点的waitStatus,如果当前节点的前置节点的waitStatus为SIGNAL,即等待通知的状态,则代表当前线程需要被挂起。如果ws大于0,那么就是cancelled状态,就可以删除了。
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;
}
如果都满足,那么就调用如下方法将线程挂起
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
这里返回interrupted方法主要是为了将线程的中断标志取消。interrupted方法是判断线程是否被打上中断标记。采用中断原语LockSupport.park对线程进行挂起的话,外部对线程的中断的操作则不会抛出异常。这里主要是为了将外部对线程打上的中断标记传递到外部。因为在等待队列中的线程是无否对外部中断作出响应的。所有需要等到线程拿到锁之后再去处理中断请求。
释放锁则调用release方法。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
这里如果尝试释放锁成功的话,那么就要去唤醒下一个节点。这里的判断条件是头节点不为空且头节点有后继节点(waitStatus为0代表当前节点没有后继节点或者是头节点)
具体看unparkSuccessor方法
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
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);
}
这里首先会释放头节点的线程,即将showStatus置为0。
然后查看下一个节点,若下一个节点为null或者showStatus为cancelled,则从尾节点向前遍历,找到一个最靠前的且showStatus为signal的节点并将其唤醒,唤醒操作为LockSupport.unpark,并将其作为head节点的下一个节点进行监听,并且该线程进行自旋操作获取锁资源。
从后往前是为了防止线程不安全的问题,在enq中,将当前节点的前置节点设置为尾节点的操作是原子性的,但是将尾节点的后继节点设置为当前节点是线程不安全的,从前往后可能会出现空指针异常(还没来得及设置)。