1. 前言:
今天有幸参与群里的AQS加锁、解锁以及同步队列的讨论。然后又撸了一遍源码,顺便写个笔记加深影响。写的不好的话还请各位客官老爷多多包涵。
2. 老生常谈的面试题:谈谈你对AQS的理解?
这个问题很多大佬都有分享自己的见解,但是不同的人表达的东西不一定适合每个人。
我觉得AQS其实就是可以让用户可以更加方便的去实现锁的一个框架。
AQS帮我们屏蔽了一些底层操作,比如:unsafe和locksupport的使用。
3. AQS的核心属性
- state:当前共享资源的加锁状态
- node:AQS内部类,用来封装加锁失败进入阻塞队列的线程
- 同步队列:由node组成双向链表
下面是AQS的源码结构,因为这一篇只讲普通加锁,所以我精简了一部分
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer{
static final class Node {
// waitStatus的值:当前节点(线程)的后续节点处于需要被唤醒的状态
// 也就是说当waitStatus等于SIGNAL的时候,当前节点还有后续节点
static final int SIGNAL = -1;
// 等待状态, Node初始化的时候是0
volatile int waitStatus;
// 前节点
volatile Node prev;
// 后节点
volatile Node next;
}
// 队列的头节点
private transient volatile Node head;
// 队列的尾节点
private transient volatile Node tail;
// 共享资源加锁状态 0:未加锁 1:加锁 大于1:重入
private volatile int state;
}
4. ReentrantLock非公平加锁源码分析
加锁代码
// 默认构造函数创建的就是非公平锁
ReentrantLock reentrantLock = new ReentrantLock();
reentrantLock.lock();
ReetrantLock的默认构造方法
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;
public ReentrantLock() {
sync = new NonfairSync();
}
}
sync继承AQS,NonfairSync是Sync的一个子类。
4.1 ReentrantLock.lock()
public class ReentrantLock implements Lock, java.io.Serializable {
public void lock() {
sync.lock();
}
}
因为这里创建的sync是 NonfairSync,所以会调用NonfairSync的lock方法
4.2 NonfairSync.lock()
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
// compareAndSetState底层调用的是 unsafe的compareAndSwapInt
// 尝试把state从0修改成1, 如果修改成功表明加锁成功。
// 并且把拥有锁的线程设置为当前线程: 解锁和判断是否是重入锁需要用到
// 这里也提醒了非公平性, 不管三七二十一上来先尝试加锁。
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
// 加锁失败
acquire(1);
}
// 再次尝试加锁, 以及处理重入锁
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
4.3 acquire(1)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这个方法主要做这几件事:
- 再次尝试加锁、以及处理重入锁
- 初始化同步队列
- 将当前线程封装成Node加入到同步队列
4.4 tryAcquire()
tryAcquire最终会调用 NonfairSync的 tryAcquire方法,也就是4.2中的。
// 再次尝试加锁, 以及处理重入锁,返回false说明加锁失败。
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) {
// 通过CAS尝试加锁
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;
}
4.5 acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
4.5.1 addWaiter(Node.EXCLUSIVE), arg)
这个方法主要是初始化队列以及将当前线程封装成node
// 传入的mode为null
private Node addWaiter(Node mode) {
// 将当前线程封装成node, 此时它的waitStatus是0
Node node = new Node(Thread.currentThread(), mode);
// 获取同步队列的尾节点
Node pred = tail;
if (pred != null) { //如果尾节点不为null, 说明之前以及初始化过同步队列
// 将尾节点设置为当前node的前节点
// 如果两个线程同时将尾节点设置为它们的前节点呢?
// 其实是没有问题的, 因为下面的 compareAndSetTail只会成功一个
node.prev = pred;
// 将当前node设置为尾节点。如果失败,则会执行enq()去把当前node设置为尾节点
if (compareAndSetTail(pred, node)) {
// 将旧的尾节点的后继节点设置为当前节点
// 这里不存在并发问题, 原因同上 compareAndSetTail 只有一个会成功
pred.next = node;
return node;
}
}
// 队列未初始化 或者 compareAndSetTail失败
enq(node);
return node;
}
// 初始化队列以及处理并发导致的 compareAndSetTail失败的逻辑
private Node enq(final Node node) {
for (;;) {
// 获取尾节点
Node t = tail;
if (t == null) { // 初始化
// 初始化完毕之后会再次进入循环
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 将当前node设置为尾节点,直到成功为止。
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
4.5.2 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);
}
}
// 判断是否需要把当前线程挂起,需要注意的是外层是自旋。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 获取前节点的waitStatus, 最开始的时候waitStatus是0
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 修改前节点的waitStatus为 -1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
// 如果 shouldParkAfterFailedAcquire 返回true,则进入该方法
private final boolean parkAndCheckInterrupt() {
// 将当前线程挂起, 等待其他线程将它唤醒。
// 唤醒之后, 如果线程没有被打断。将再次执行外层的自旋, 然后尝试加锁
LockSupport.park(this);
return Thread.interrupted();
}
自此,加锁方法分析完毕。
5. 非公平锁解锁源码分析
release()
// 解锁
public final boolean release(int arg) {
// 尝试解锁
if (tryRelease(arg)) {
// 获取头节点
Node h = head;
// 判断是否需要唤醒头节点的next节点
// 当waitStatus != 0 的时候说明还有后续节点
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
tryRelease()
主要是修改state值, 需要判断是否是重入锁
protected final boolean tryRelease(int releases) {
// 当 c > 0 的时候, 说明当前是重入锁
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
unparkSuccessor()
唤醒头节点的后节点,也就是队列中的第二个节点。
// 唤醒节点
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0){
// 通过CAS设置首节点的waitStatus
compareAndSetWaitStatus(node, ws, 0);
}
// 获取下一个节点
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);
}
唤醒之后,第二个节点的线程就会继续开始运行,我们先来看下线程是在哪里被挂起的
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);
}
}
自此,解锁完毕。