AbstractQueueSynchronizer之AQS【重要】
前置知识
公平锁和非公平锁
公平锁:锁被释放以后,先申请的线程先得到锁。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁
非公平锁:锁被释放以后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁
可重入锁
也叫做递归锁,指的是线程可以再次获取自己的内部锁,比如一个线程获取到了对象锁,此时这个对象锁还没有释放,当其想再次获取这个对象锁的时候还是可以获取的,如果不可重入的话,会导致死锁
自旋思想
当线程请求锁时,如果锁已经被其他线程持有,那么该线程会不断地重试获取锁,而不是被挂起等待,这种不断尝试获取锁的行为称为自旋
LockSupport
一个工具类,用于线程的阻塞和唤醒操作,类似于wait()和notify()方法,但是更加灵活和可控
提供了park()和unpark()两个静态方法用于线程阻塞和唤醒操作
优点在于可以在任意时刻阻塞和唤醒线程而不需要事先获取锁或监视器对象
数据结构之双向链表
双向链表(Doubly Linked List)是一种常见的数据结构,它是由一系列结点(Node)组成的,每个结点包含三个部分:数据域、前驱指针和后继指针。其中,数据域存储结点的数据,前驱指针指向前一个结点,后继指针指向后一个结点。通过这种方式,双向链表可以实现双向遍历和插入、删除操作
设计模式之模板设计模式
模板设计模式是一种行为型设计模式,定义了一种算法的框架,并将某些步骤延迟到子类中实现,这种设计模式的主要目的是允许子类在不改变算法结构的情况下重新定义算法中的某些步骤。优点是能够提高代码复用性和可维护性
AQS入门级别理论知识
是什么?
字面意思:抽象的队列同步器
源代码
通常地,AbstractQueuedSynchronizer简称为AQS
技术解释
AQS是用来实现锁或者其他同步器组件的公共基础部分的抽象实现;是重量级基础框架及整个JUC体系的基石,只要用于解决锁分配给”谁“的问题
官网解释
具体分析
整体就是一个抽象的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量表示持有锁的状态
AQS为什么是JUC内容中最重要的基石
和AQS有关的类
- java.util.concurrent.locks.ReentrantLock
- java.util.concurrent.CountDownLatch
- java.util.concurrent.locks.ReentrantReadWriteLock
- java.util.concurrent.Semaphore
- …
ReentrantLock
CountDownLatch
ReentrantReadWriteLock
Semaphore
…
进一步理解锁和同步器的关系
锁是面向锁的使用者
定义了程序员和锁交互的使用层API,隐藏了实现细节,你调用即可
同步器是面向锁的实现者
Java并发大神DoungLee,提出了统一规范并简化了锁的实现,将其抽象出来,屏蔽了同步状态管理、同步队列的管理和维护、阻塞线程排队和通知、唤醒机制等,是一切锁和同步组件实现的公共基础部分
能干嘛?
加锁会导致阻塞——有阻塞就需要排队,实现排队必然需要队列
解释说明
抢到资源的线程直接使用处理业务,抢不到资源的必然涉及一种排队等候机制。抢占资源失败的线程继续去等待(类似于银行办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等待),但等候线程仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)
既然说到了排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢?
如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS同步队列的抽象表现。它将要请求共享资源的线程及自身的等待状态封装成队列的节点对象(Node),通过CAS、自旋以及LockSupport.park()的方式,维护着state变量的状态,使其达到同步的状态
源码查看
小总结
AQS同步队列的基本结构
AQS源码分析前置知识储备
AQS内部体系架构图
AQS自身
AQS的同步状态int类型State成员变量
/**
* The synchronization state.
*/
private volatile int state;
类比生活:银行办理业务的受理窗口状态
- 零就是没人,自由状态可以去办理
- 大于等于1,有人占用窗口,等着去
AQS的CLH队列
CLH(三个大牛的名字组成)队列为一个双向队列
小总结
有阻塞就需要排队,实现排队必然需要队列
AQS自身:state变量+CLH双端队列
内部类Node
Node的int变量
Node的等待状态waitState成员变量
/**
* Status field, taking on only the values:
* SIGNAL: The successor of this node is (or will soon be)
* blocked (via park), so the current node must
* unpark its successor when it releases or
* cancels. To avoid races, acquire methods must
* first indicate they need a signal,
* then retry the atomic acquire, and then,
* on failure, block.
* CANCELLED: This node is cancelled due to timeout or interrupt.
* Nodes never leave this state. In particular,
* a thread with cancelled node never again blocks.
* CONDITION: This node is currently on a condition queue.
* It will not be used as a sync queue node
* until transferred, at which time the status
* will be set to 0. (Use of this value here has
* nothing to do with the other uses of the
* field, but simplifies mechanics.)
* PROPAGATE: A releaseShared should be propagated to other
* nodes. This is set (for head node only) in
* doReleaseShared to ensure propagation
* continues, even if other operations have
* since intervened.
* 0: None of the above
*
* The values are arranged numerically to simplify use.
* Non-negative values mean that a node doesn't need to
* signal. So, most code doesn't need to check for particular
* values, just for sign.
*
* The field is initialized to 0 for normal sync nodes, and
* CONDITION for condition nodes. It is modified using CAS
* (or when possible, unconditional volatile writes).
*/
volatile int waitStatus;
类比生活:等候区其他顾客(其他线程)的等待状态,队列中每个排队的个体就是一个Node
Node此类的讲解
内部结构
属性说明
AQS源码深度讲解和分析
ReentrantLock的原理
ReentrantLock的内部体系架构图
Lock接口的实现类,基本都是通过聚合了一个队列同步器的子类完成线程访问控制的
从最简单的lock方法开始看看公平和非公平
公平锁和非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors()——公平锁加锁时判断等待队列中是否存在排队的线程
/**
* 查询是否有线程等待获取的时间比当前线程长
* 如果当前线程之前有一个排队的线程,返回true;如果当前线程处于队列的头部或队列为空,则返回false
*/
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
以非公平锁ReentrantLock()为例作为突破走起——方法lock()
对比公平锁和非公平锁的tryAcquire()方法的实现代码,其实差异就在于非公平锁获取锁时比公平锁中少了一个判断:hasQueuedPredecessors(),hasQueuedPredecessors()中判断了是否需要排队
导致公平锁和非公平锁的差异如下:
公平锁:公平锁讲究先来后到,线程在获取锁时,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入到等待队列中;
非公平锁:不管是否有等待队列,如果可以获取到锁,则立刻占有锁对象。也就是说队列的第一个排队线程苏醒后,不一定就是排头的这个线程获得锁,它还需要参加竞争锁(存在线程竞争的情况下),后来的线程可能不讲武德插队夺锁了
以非公平锁ReentrantLock()为例正式开始源码解读(公平锁同理)
lock()
final void lock() {
// cas修改state成员变量
if (compareAndSetState(0, 1))
// 修改成功,设置独占锁的线程为当前线程
setExclusiveOwnerThread(Thread.currentThread());
else
// 修改失败,当前线程抢占失败,排队等待后续抢占
acquire(1);
}
acquire()
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
// Node.EXCLUSIVE表示独占模式
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// 非公平锁的tryAcquire()方法实现
// true抢锁成功;false抢锁失败
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取state成员变量
int c = getState();
// 独占锁没有被占用
if (c == 0) {
// cas修改state成员变量
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");
// 把锁的state成员变量再+1
// 相当于当前线程重复获取锁,ReentrantLock是可重入锁
setState(nextc);
return true;
}
return false;
}
// 为当前线程和给定模式创建排队节点
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;
}
// 入队
private Node enq(final Node node) {
for (;;) {
// tail为尾节点
Node t = tail;
// 尾节点为null,初始化队列
if (t == null) { // Must initialize
// 初始化队列设置头节点并不是传入的节点,而是new Node(),
// 新创建叫做虚拟节点或者哨兵节点,作用就是占位,而且Thread=null,waitStatus=0
// 注意:双向链表中,第一个节点为虚拟节点(也叫哨兵节点),其实并不存储任何信息,只是占位,真正的第一个有数据的节点,是从第二个节点开始的
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 传入的节点的prev节点指向tail节点
node.prev = t;
// 传入的节点变为尾节点
if (compareAndSetTail(t, node)) {
// 把tail的next节点指向传入的节点
t.next = node;
return t;
}
}
}
}
// 已经在队列中的当前线程尝试获取独占锁
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 获取当前节点的前一个节点,声明为p
final Node p = node.predecessor();
// 如果p节点为头节点,且再次尝试获取锁成功
if (p == head && tryAcquire(arg)) {
// 设置头节点为当前节点
setHead(node);
// p节点的后一个节点设置为null
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 异常情况,取消正在进行的获取独占锁的线程节点,出队
if (failed)
cancelAcquire(node);
}
}
// 返回前一个节点,如果为空则抛出NullPointerException
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
// 检查并更新获取失败节点的状态。如果线程阻塞,返回true。这是所有循环获取锁环路中的主要信号控制。要求pred == node.prev。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 获取前置节点的状态
int ws = pred.waitStatus;
// 如果是SIGNAL状态,即等待被占用的资源释放,直接返回true
// 准备继续调用parkAndCheckInterrupt()方法
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
// ws>0说明是CANCELLED状态
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
// 循环判断前置节点的前置节点是否也是CANCELLED状态,忽略该状态的节点,重新连接队列
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
// waitStatus一定为0或PROPAGATE。表示我们需要信号,先不要停车。调用方需要重试以确保在停车前无法获取锁
} 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.
*/
// 设置前置节点的waitStatus为Node.SIGNAL=-1,用于后续唤醒操作
// 程序第一次执行到这返回为false,还会进行外层第二次循环,最终从if (ws == Node.SIGNAL)返回true
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
// 方便方法停车,然后检查是否中断
private final boolean parkAndCheckInterrupt() {
// 挂起当前线程,使得当前线程停留在队列中
// 根据 park 方法 API描述,程序在下达三种情况会继续向下执行
// 1.unpark
// 2.被中断(interrupt)
// 3.其他不合逻辑的返回才会继续向下执行
LockSupport.park(this);
// 因上述三种情况程序执行至此,返回当前线程的中断状态,并清空中断状态l/ 如泉由于被中断,该方法会返回 true
return Thread.interrupted();
}
// 取消正在进行的获取独占锁的线程节点
// 不想等了,离队
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
node.thread = null;
// 获取传入节点的前一个节点
// Skip cancelled predecessors
Node pred = node.prev;
// 如果传入节点的前一个节点的waitStatus > 0
// 这种情况是存在多个节点同时离队
// 需要找到最近的一个不离队的节点赋值给node.prev
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// predNext is the apparent node to unsplice. CASes below will
// fail if not, in which case, we lost race vs another cancel
// or signal, so no further action is necessary.
// 获取传入节点的前一个节点的下一个节点
Node predNext = pred.next;
// Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
// 设置传入节点的waitStatus为Node.CANCELLED:表示线程获取锁的请求已经取消
node.waitStatus = Node.CANCELLED;
// If we are the tail, remove ourselves.
// 如果传入节点是尾节点,且传入节点的前一个节点设置为尾节点成功
if (node == tail && compareAndSetTail(node, pred)) {
// 将传入节点的前一个节点的下一个节点设置为null
compareAndSetNext(pred, predNext, null);
} else {
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
// 传入节点的前一个节点不是头节点
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
// 获取传入节点的下一个节点
Node next = node.next;
// 如果传入节点的下一个节点不为null,且传入节点的下一个节点的waitStatus <= 0
if (next != null && next.waitStatus <= 0)
// 将传入节点的前一个节点的下一个节点设置为传入节点的下一个节点
compareAndSetNext(pred, predNext, next);
} else {
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
// 当前线程自中断
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
unlock()
// 试图释放此锁
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
// 尝试释放锁
if (tryRelease(arg)) {
Node h = head;
// 如果头节点不为null,且waitStatus不为0
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
// 当前线程占用锁getState()为1,所以c=0
int c = getState() - releases;
// 如果当前线程不等于持有锁的线程,抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
// 设置独占锁的所有者线程为null
setExclusiveOwnerThread(null);
}
// 设置当前线程的state为c=0
setState(c);
return free;
}
// 唤醒节点的后继者(如果存在)
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;
// 如果传入节点的waitStatus < 0,重新设置为0
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;
// 下一个节点为null或者waitStatus > 0
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;
}
// 下一个节点不为null,唤醒下一个节点
if (s != null)
// 之前的parkAndCheckInterrupt()方法就会return Thread.interrupted();返回false
// 之后下一个节点继续执行acquireQueued()方法的循环,执行p == head && tryAcquire(arg)尝试获取锁
LockSupport.unpark(s.thread);
}
代码案例
package com.bilibili.juc.aqs;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class AQSDemo {
public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock(); // 非公平锁
// A B C三个顾客,去银行办理业务,A先到,此时窗口空无一人,他优先获取办理业务的机会
// A 严重耗时,估计长期占有窗口
new Thread(() -> {
reentrantLock.lock();
try {
System.out.println("--------come in A--------");
// A线程暂停20分钟
try {
TimeUnit.MINUTES.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
reentrantLock.unlock();
}
}, "A").start();
// B 是第二个顾客,B一看到受理窗口被A占用,只能去候客区等待,进入AQS队列,等待A办理完成,尝试去抢占受理窗口
new Thread(() -> {
reentrantLock.lock();
try {
System.out.println("--------come in B--------");
} finally {
reentrantLock.unlock();
}
}, "B").start();
// C 是第三个顾客,B一看到受理窗口被A占用,只能去候客区等待,进入AQS队列,等待A办理完成,尝试去抢占受理窗口,前面是B顾客,FIFO
new Thread(() -> {
reentrantLock.lock();
try {
System.out.println("--------come in C--------");
} finally {
reentrantLock.unlock();
}
}, "C").start();
// 后续顾客DEFG......以此类推
new Thread(() -> {
reentrantLock.lock();
try {
System.out.println("--------come in D--------");
} finally {
reentrantLock.unlock();
}
}, "D").start();
}
}
A线程还在占用锁,B、C线程等待的结果图
A线程释放锁,B线程获取锁结果图
总结ReentrantLock()的加锁过程主要分为三个阶段
- 尝试加索
- 加锁失败,线程入队列
- 线程入队列后,进入阻塞状态
公平锁ReentrantLock()的是否需要排队方法hasQueuedPredecessors()
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
// 获取尾节点
Node t = tail; // Read fields in reverse initialization order
// 获取头节点
Node h = head;
Node s;
// 队列未初始化,此时h==t==null,h!=t返回false,整个方法返回false,表示不需要排队
// 队列已经初始化,如果队列元素个数=1,当最后一个排队线程也获取锁之后,此时head==tail!=null,排队队列如下图,h!=t返回false,整个方法返回false,表示不需要排队
// 队列已经初始化,如果队列元素个数>1,则至少2个元素,h.next指向第二个元素,肯定不是null,所以(s = h.next) == null返回false,s.thread != Thread.currentThread()返回true,表示两者不相等,说明下一个可以获得锁的线程不是当前线程,所以整个方法返回true,表示需要排队
// 队列已经初始化,如果队列元素个数>1,则至少2个元素,h.next指向第二个元素,肯定不是null,所以(s = h.next) == null返回false,s.thread != Thread.currentThread()返回false,(s是第二个节点(第一个节点是占位空节点),代表下一个应该获取锁的线程)表示两者相等,即下一个可以获得锁的线程正是当前线程,所以整个方法返回false,表示不需要排队
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}