文章目录
概述
AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,ReentrantLock,Semaphore,ReentrantReadWriteLock,SynchronousQueue,FutureTask
等等皆是基于AQS的。
AQS 核心思想是,如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是 CLH 队列的变体实现的,将暂时获取不到锁的线程加入到队列中。
原理
AQS 使用一个 Volatile 的 int 类型的成员变量来表示同步状态,通过内置的 FIFO 队列来完成资源获取的排队工作,通过 CAS 完成对 State 值的修改。
AQS维护一个共享变量state,来表示同步状态
private volatile int state;//共享变量,使用volatile修饰保证线程可见性
状态信息通过procted类型的getState,setState,compareAndSetState
进行操作。可以通过修改 State 字段表示的同步状态来实现多线程的独占模式和共享模式(加锁过程)
AQS 定义两种资源共享方式:
Exclusive
(独占,只有一个线程能执行,如ReentrantLock)Share
(共享,多个线程可同时执行, 如Semaphore/CountDownLatch)
- 独占式
- 共享式
AQS通过CLH"队列来完成获取资源线程的排队工作
(Craig、Landin and Hagersten 队列,是单向链表,AQS使用CLH的变体,为双向链表)。
该队列由一个一个的Node结点
组成,每个Node结点维护一个prev引用和next引用,分别指向自己的前驱和后继结点。AQS维护两个指针,分别指向队列头部head和尾部tail。线程被封装成Node进入CLH队列并阻塞。
自定义同步器
自定义同步器实现的相关方法也只是为了通过修改 State 字段来实现多线程的独占模式或者共享模式
AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的钩子方法:
钩子方法是一种被声明在抽象类中的方法,一般使用 protected 关键字修饰,它可以是空方法(由子类实现),也可以是默认实现的方法。模板设计模式通过钩子方法控制固定步骤的实现。
//独占方式。尝试获取资源,成功则返回true,失败则返回false。
protected boolean tryAcquire(int)
//独占方式。尝试释放资源,成功则返回true,失败则返回false。
protected boolean tryRelease(int)
//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
protected int tryAcquireShared(int)
//共享方式。尝试释放资源,成功则返回true,失败则返回false。
protected boolean tryReleaseShared(int)
//该线程是否正在独占资源。只有用到condition才需要去实现它。
protected boolean isHeldExclusively()
不可重写的方法
/*Acquires in exclusive mode, ignoring interrupts.*/
//独占式获取同步状态,如果当前线程获取同步状态成功,立即返回。否则,将会进入同步队列等待,
//该方法将会重复调用重写的tryAcquire(int arg)方法
public final void acquire(int arg)
/*Acquires in exclusive mode, aborting if interrupted.*/
//与acquire(int arg)基本相同,但是该方法响应中断。
public final void acquireInterruptibly(int arg)
/* Releases in exclusive mode. Implemented by unblocking one or more threads if {@link #tryRelease} returns true.
This method can be used to implement method {@link Lock#unlock}.*/
//独占式释放同步状态,该方法会在释放同步状态后,将同步队列中第一个节点包含的线程唤醒
public final boolean release(int arg)
通过调用同步器的acquire(int arg)方法可以获取同步状态
public final void acquire(int arg) {//**该方法是模板方法**
if (!tryAcquire(arg) &&//先通过tryAcquire获取同步状态
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//获取同步状态失败则生成节点并加入同步队列
selfInterrupt();
}
- 首先,调用使用者重写的
tryAcquire
方法,若返回true,意味着获取同步状态成功,后面的逻辑不再执行;若返回false,也就是获取同步状态失败,进入2步骤; - 此时,获取同步状态失败,构造独占式同步结点,通过
addWatiter
将此结点添加到同步队列的尾部,此时可能会有多个线程结点试图加入同步队列尾部,需要以线程安全的方式添加:compareAndSetTail(Node expect,Node update)
它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联。
//将节点加入到同步队列的尾部
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);//生成节点(Node)
// Try the fast path of enq; backup to full enq on failure
//快速尝试在尾部添加
Node pred = tail;
if (pred != null) {
node.prev = pred;//先将当前节点node的前驱指向当前tail
if (compareAndSetTail(pred, node)) {//CAS尝试将tail设置为node
//如果CAS尝试成功,就说明"设置当前节点node的前驱"与"CAS设置tail"之间没有别的线程设置tail成功
//只需要将"之前的tail"的后继节点指向node即可
pred.next = node;
return node;
}
}
enq(node);//否则,通过死循环来保证节点的正确添加
return node;
}
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)) {//直到CAS成功为止
t.next = node;
return t;//结束循环
}
}
}
}
- 最后调用
acquireQueued
(Node node,int arg)方法,使得该节点以“死循环”的方式获取同步状态,若获取不到,则阻塞结点线程LockSupport.park(...)
,直到被前驱结点唤醒或者被中断。
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);
}
}
/**Checks and updates status for a node that failed to acquire.
* Returns true if thread should block. This is the main signal control in all acquire loops.*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;//获取前驱节点的等待状态
if (ws == Node.SIGNAL)
//SIGNAL状态:前驱节点释放同步状态或者被取消,将会通知后继节点。因此,可以放心的阻塞当前线程,返回true。
/* 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 {//独占模式下,一般情况下这里指前驱节点等待状态为SIGNAL
/* 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);//设置当前节点等待状态为SIGNAL
}
return false;
}
/** Convenience method to park and then check if interrupted 。return {@code true} if interrupted */
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);//阻塞当前线程
return Thread.interrupted();
}
总的流程图,这也是ReentrantLock.lock()
的流程:
通过调用同步器的release(int arg)方法可以释放同步状态
public final boolean release(int arg) {
if (tryRelease(arg)) {//释放同步状态
Node h = head;
if (h != null && h.waitStatus != 0)//独占模式下这里表示SIGNAL
unparkSuccessor(h);//唤醒后继节点
return true;
}
return false;
}
/** Wakes up node's successor, if one exists.*/
private void unparkSuccessor(Node node) {
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) {//找到第一个没有被取消的后继节点(等待状态为SIGNAL)
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);//唤醒后继线程
}
该方法在释放了同步状态之后,会"唤醒"其后继节点(进而使后继节点重新尝试获取同步状态)LockSupport.unpark(...)
具体应用
ReentrantLock
这篇博客写得实在是太详细了: https://blog.csdn.net/wwj17647590781/article/details/117574639?spm=1001.2014.3001.5501
Semaphore(信号量)
synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,而Semaphore(信号量)可以用来控制同时访问特定资源的线程数量。
Semaphore 通常用于那些资源有明确访问数量限制的场景比如限流(仅限于单机模式,实际项目中推荐使用 Redis +Lua 来做限流)。
// 初始共享资源数量
final Semaphore semaphore = new Semaphore(5);
// 获取1个许可
semaphore.acquire();
// 释放1个许可
semaphore.release();
以无参 acquire 方法为例,调用semaphore.acquire()
,线程尝试获取许可证,
- 如果 state > 0 的话,则表示可以获取成功。尝试使用 CAS 操作去修改 state 的值 state=state-1
- 如果 state <= 0 的话,则表示许可证数量不足,会创建一个 Node 节点加入等待队列,挂起当前线程
以无参 release 方法为例,调用semaphore.release()
,线程尝试释放许可证,并使用 CAS 操作去修改 state 的值 state=state+1。释放许可证成功之后,同时会唤醒等待队列中的一个线程。被唤醒的线程会重新尝试去修改 state 的值 state=state-1 ,如果 state > 0 则获取令牌成功,否则重新进入等待队列,挂起线程。
CountDownLatch
CountDownLatch 允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。
CountDownLatch 的两种典型用法:
- 某一线程在开始运行前等待 n 个线程执行完毕 : 将 CountDownLatch 的计数器初始化为 n (new CountDownLatch(n)),每当一个任务线程执行完毕,就将计数器减 1 (
countdownlatch.countDown()
),当计数器的值变为 0 时,在 CountDownLatch 上await()
的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。主线程.await() - 实现多个线程开始执行任务的最大并行性:注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的 CountDownLatch 对象,将其计数器初始化为 1 (new CountDownLatch(1)),多个线程在开始执行任务前首先 coundownlatch.await(),当主线程调用 countDown() 时,计数器变为 0,多个线程同时被唤醒。非主线程.await()
public class CountDownLatchExample {
// 请求的数量
private static final int THREAD_COUNT = 550;
public static void main(String[] args) throws InterruptedException {
// 创建一个具有固定线程数量的线程池对象(如果这里线程池的线程数量给太少的话你会发现执行的很慢)
// 只是测试使用,实际场景请手动赋值线程池参数
ExecutorService threadPool = Executors.newFixedThreadPool(300);
final CountDownLatch countDownLatch = new CountDownLatch(THREAD_COUNT);
for (int i = 0; i < THREAD_COUNT; i++) {
final int threadNum = i;
threadPool.execute(() -> {
try {
test(threadNum);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 表示一个请求已经被完成
countDownLatch.countDown();
}
});
}
countDownLatch.await();
threadPool.shutdown();
System.out.println("finish");
}
public static void test(int threadnum) throws InterruptedException {
Thread.sleep(1000);
System.out.println("threadNum:" + threadnum);
Thread.sleep(1000);
}
}
CyclicBarrier(循环栅栏)
它要做的事情是:让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
CountDownLatch
的实现是基于AQS
的,而CycliBarrier
是基于ReentrantLock
(ReentrantLock 也属于 AQS 同步器)和Condition
的。
parties为每次拦截的线程数
count作为计数器,每当一个线程到了栅栏这里,就将计数器-1,当count==0时,表示这一代的最后一个线程到达栅栏,执行构造器中输入的任务。
//每次拦截的线程数
private final int parties;
//计数器
private int count;
当调用 CyclicBarrier 对象的 await() 方法时,实际上调用的是 dowait(false, 0L)方法。 await() 方法就像树立起一个栅栏的行为一样,将线程挡住了,当拦住的线程数量达到 parties 的值时,栅栏才会打开,线程才得以通过执行。然后开启下一波栅栏。