摘要:该文章深入探讨了Java并发编程中的关键组件之一,即AQS(AbstractQueuedSynchronizer),并对其源代码进行了详细解读。AQS是Java并发库中用于实现锁和同步器的重要基类。文章首先介绍了AQS的基本概念和工作原理,然后深入分析了AQS的核心方法和数据结构。通过对源码的逐行解析,文章解释了AQS如何使用内部的双向链表(CLH队列)来维护线程的等待队列,并通过状态变量来管理线程的获取和释放。此外,文章还涵盖了AQS的独占模式和共享模式的实现细节
AQS简介
在多线程编程中,AQS(AbstractQueuedSynchronizer)是一个重要的同步器框架。它提供了一种基于队列的方式来实现同步操作,使用了一个等待队列来管理线程的竞争和等待。
AQS的核心思想是通过一个整数状态变量(state)来表示共享资源的状态,并使用CAS(Compare and Swap)操作来实现对状态的原子更新。AQS提供了两种模式的同步器:独占模式(Exclusive mode)和共享模式(Shared mode)。
在AQS中,每个线程通过acquire操作来请求获取同步资源,如果资源已经被占用,则线程会进入等待队列并被阻塞。当资源释放时,AQS会从等待队列中选择一个或多个线程唤醒,并允许其继续执行。
AQS的具体实现是通过内部的Node节点和等待队列来实现的。每个等待获取同步资源的线程都会被封装成一个Node节点,并按照获取资源的顺序链接到等待队列中。如果一个线程成功获取到资源,则它会成为同步队列的头节点。
-
独占模式(Exclusive Mode):
在独占模式下,只有一个线程能够获得资源的访问权,其他线程需要等待该线程释放资源才能继续访问。在Java并发库中,独占模式常用于实现互斥锁(Mutex)或者排他锁(Exclusive Lock),例如ReentrantLock类就是一种独占模式的锁。当一个线程获得独占锁后,其他线程会被阻塞,直到该线程释放锁。 -
共享模式(Shared Mode):
在共享模式下,多个线程可以同时获得资源的访问权,它们可以并发地执行对资源的操作。共享模式常用于实现读写锁(ReadWrite Lock),其中读操作可以并发执行,而写操作需要独占资源。在Java并发库中,ReentrantReadWriteLock类提供了对读写锁的支持。在读写锁中,多个线程可以同时获取读锁,但只有一个线程可以获取写锁,读锁和写锁之间是互斥的,即写锁被持有时,其他线程不能获取读锁或写锁。
ReentrantLock
每个ReentLock类中维护了一个继承自AbstractQueuedSynchronizer(aqs)的Sync类
加锁过程:
1 首先线程调用lock方法
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
线程尝试利用cas机制来修改aqs抽象类中的state字段,这是一个被volatile修饰的整形,当成功修饰后此线程不会被阻塞即视为获得锁
接下来假设还有线程要获得锁,那么就会进入acquire(1)方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
首先会再次尝试获取,假设失败后就会进入addWaiter方法,这个方法是利用链表创建了一个创建了队列,每次堵塞线程被设置为node的字段然后加入阻塞队列中
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);
}
}
堵塞的实现主要在于parkAndCheckInterrupt(),其方法内部调用LockSuport.park()方法让当前线程阻塞
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
这里是阻塞的实现接下来是不可打断的实现
当其他地方调用当前线程的intereupte方法时,由于park方法可被打断就再次进入循环尝试获取锁,只用获取到锁时才会返回是否被打断过
假设途中被打断过,并且线程被unpark方法唤醒,那么就会在for(;;)循环中执行获取锁的方法,假设获取锁成功,被打断过的方法唯一不同的是返回的interrupted的值为true,然后执行上一级方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
,由于interrupted方法会把线程打断状态值为false,这里再次调用interrupt方法,标记打断状态,进而让用户自己做一些操作
可以看到只用得到锁的情况下才会执行,这就是为什么锁不可被打断
给定的cancelAcquire
方法实现了在获取同步资源过程中取消节点(线程)的操作。以下是该方法的作用:
- 首先,如果传入的节点
node
为null
,表示节点不存在,直接返回,不进行任何操作。 - 将节点
node
的线程引用node.thread
置为null
,表示该节点对应的线程已被取消。 - 跳过已被取消的前驱节点,将节点
node
的前驱节点pred
设置为最近一个状态不为CANCELLED
的前驱节点。 - 获取前驱节点
pred
的后继节点predNext
,这是一个暂时的后继节点,用于后续的操作。 - 将节点
node
的等待状态node.waitStatus
设置为CANCELLED
,表示节点已被取消。 - 如果节点
node
是尾节点(即node == tail
),则尝试原子地将尾节点设置为前驱节点pred
,如果成功则将前驱节点的下一个节点predNext
设置为null
,从而移除节点node
。 - 如果节点
node
不是尾节点,则检查前驱节点pred
的等待状态pred.waitStatus
,如果等待状态为SIGNAL
或非正数(小于等于0),并且前驱节点的线程pred.thread
不为null
,则尝试将前驱节点的等待状态设置为SIGNAL
,以确保后继节点会得到信号。如果设置成功,则检查节点node
的下一个节点node.next
,如果不为null
并且等待状态node.next.waitStatus
小于等于0,则尝试原子地将前驱节点的下一个节点predNext
设置为节点node
的下一个节点node.next
。 - 如果前述步骤都不满足,则调用
unparkSuccessor(node)
方法唤醒后继节点,以便传播信号。 - 最后,将节点
node
的下一个节点node.next
设置为自身,用于帮助垃圾回收。
总体而言,cancelAcquire
方法的作用是取消节点(线程)的获取同步资源的操作,并进行相应的处理,包括移除节点、传播信号或唤醒后继节点等。
可打断锁原理
调用ReentLock的lockInterruptibly()方法
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
进一步查看doAcquireInterruptibly(arg);的源码和不可打断锁源码差不多
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
重点在于当被打断时这里是直接抛出异常,而不在方法内部捕获交由用户自己处理
重入锁
当调用lock方法时会调用tryAcquire方法。
这里的getState就是获得aqs抽象类的state字段,如果是0就就说明当前没有线程获得锁,利用cas机制进行赋值(这个操作是原子性的),假设此时有线程再次加锁,就会判断当前线程和获得锁的线程是不是一致如果一致的话就会在state的基础上加一。这就是可重入锁的原理
公平锁和非公平锁
非公平锁是指当阻塞队列中的线程被唤醒后(只唤醒一个),可能会有新的线程和它去竞争,这样就不保证先到先得到
公平锁是指当线程被唤醒,只有排在阻塞队列最前方的线程才能获取锁
非公平锁的实现
当调用lock方法后
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
会调用tryAcquire方法,这里有一个hasQueuedPredecessors方法,用来判断当前线程是不是位于头节点后的第二个线程,如果是就返回true,进而就不用进入阻塞,否则就进入阻塞队列。这就是公平锁的实现
条件变量:
首先会创建一个Condition对象返回,本质还是创建利用链表创建队列
当调用条件变量的awit方法后
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
首先创建新的node节点,接下来调用fullyRelease方法释放当前线程的锁(包括可重入锁),接下来进入阻塞
当调用signal方法后,会进而调用doSignal
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
首先就是要当前节点出队列,然后把当前节点利用transferForSignal方法转移到公平锁或者非公平锁的队列中
compareAndSetWaitStatus(node, Node.CONDITION, 0)把当前节点的状态设置为0
enq把当前节点加入阻塞队列并返回前一个节点接下来调用
compareAndSetWaitStatus(p, ws, Node.SIGNAL)把前一个节点的状态设置为-1,因为节点状态是-1同时表示要唤醒后面的节点
锁释放
protected final boolean tryRelease(int releases) {
//每次释放就让state的值减去1
int c = getState() - releases;
//如果不是当前线程调用的unlock方法就会抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//如果锁释放完毕就把state的值设置为0,并把当前线程的字段设置为null
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
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(h)方法
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);
}
唤醒队列中的头节点后的第一个节点。到这里就和公平锁和非公平锁联系了起来