JUC并发编程与源码分析笔记13-AbstractQueuedSynchronizer之AQS

news/2024/10/30 23:19:20/

前置知识

  • 公平锁和非公平锁
  • 可重入锁
  • 自旋思想
  • LockSupport
  • 数据结构之双向链表
  • 设计模式之模板设计模式

AQS入门级别理论知识

是什么

AbstractQueuedSynchronizer:抽象的队列同步器。
用来实现锁或其他同步器组件的公共基础部分的抽象实现,是重量级基础框架及整个JUC体系的基石,主要用于解决锁分给谁的问题。
为了实现阻塞锁和相关的同步器提供的一个框架,依赖于一个先进先出的等待队列。依靠单个原子int值表示状态,通过占用和释放方法改变状态值。
在这里插入图片描述

AQS为什么是JUC内容中最重要的基石

和AQS有关的

在这里插入图片描述
ReentrantLock
在这里插入图片描述
CountDownLatch
在这里插入图片描述
ReentrantReadWriteLock
在这里插入图片描述
Semaphore
在这里插入图片描述

进一步理解锁和同步器的关系

锁:面向锁的使用者,定义了程序员和锁交互的使用层API,隐藏了实现细节,只需要调用即可。
同步器:面向锁的实现者,DougLee提出了统一规范并简化锁的实现,将其抽象出来屏蔽了同步状态管理、同步队列的管理和维护、阻塞线程排队和通知、唤醒机制等,是一切锁和同步组件实现的公共基础部分。

能干嘛

抢到资源的线程直接使用处理业务,抢不到资源的必然涉及一种排队等候机制。抢占资源失败的线程继续去等待(类似银行业务办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等候),但等候线程仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)。
加锁会导致阻塞,进而需要排队,就需要有队列。
如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS同步队列的抽象表现。它将要请求共享资源的线程及自身的等待状态封装成队列的结点对象(Node),通过CAS、自旋以及LockSupport.park()的方式,维护state变量的状态,使并发达到同步的效果。
AQS使用一个volatile的int类型的成员变量state表示同步状态,通过内置FIFO队列完成资源获取的排队工作,将每条要去抢占资源的线程封装成一个Node结点来实现锁的分配,通过CAS完成对State值的修改。

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {static final class Node {volatile int waitStatus;volatile java.util.concurrent.locks.AbstractQueuedSynchronizer.Node prev;volatile java.util.concurrent.locks.AbstractQueuedSynchronizer.Node next;}private transient volatile java.util.concurrent.locks.AbstractQueuedSynchronizer.Node head;private transient volatile java.util.concurrent.locks.AbstractQueuedSynchronizer.Node tail;private volatile int state;protected final int getState() {return state;}protected final void setState(int newState) {state = newState;}
}

小总结

在这里插入图片描述

AQS源码分析前置知识储备

在这里插入图片描述

AQS自身

AQS的int变量

AQS的同步状态state成员变量

/*** The synchronization state.* state=0:可以操作* state>0:有线程占用*/
private volatile int state;

AQS的CLH队列

CLH队列,是一个双向队列。尾部入队,头部出队。

小总结

阻塞→排队→队列=state变量+CLH双端队列。

内部类Node(Node类是AQS的内部类)

Node的int变量

/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED =  1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL    = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
/*** waitStatus value to indicate the next acquireShared should* unconditionally propagate*/
static final int PROPAGATE = -3;
/*** 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源码深度讲解和分析

Lock接口的实现类,基本都是通过聚合了一个队列同步器的子类完成线程访问控制的。

ReentrantLock的原理

Lock.java是一个接口,在创建Lock对象的时候,这里拿Lock的实现类ReentrantLock.java为例。
ReentrantLock创建对象的时候,可以传true/false,也可以不传(等价于false),从而创建出来公平锁和非公平锁。

从最简单的lock()方法开始看看公平和非公平

在执行lock()的时候,公平锁和非公平锁执行逻辑会不一样,顺着lock()方法点进去。
lock()sync.lock()acquire()
公平锁:

final void lock() {acquire(1);
}protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {if (!hasQueuedPredecessors() &&compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0)throw new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;
}

非公平锁:

final void lock() {if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);
}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) {if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;
}

观察公平锁和非公平锁的tryAcquire()方法,公平锁多了一个语句:!hasQueuedPredecessors(),用来判断当前队列是否有前驱。
公平锁:先来先到,线程在获取锁的时候,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入等待队列。
非公平锁:不管是否有等待队列,如果可以获取锁,则立刻占有锁对象,也就是队列的第一个排队线程苏醒后,不一定是排头的这个线程获取的锁,它还是需要参加竞争锁(如果线程竞争的情况),后来的线程就插队夺锁了。

以非公平锁ReentrantLock为例作为突破,了解lock()方法

以非公平锁作为案例突破,解读源码。

import java.util.concurrent.locks.ReentrantLock;public class AQSDemo {public static void main(String[] args) {// A、B、C去银行办理业务,只有一个窗口ReentrantLock reentrantLock = new ReentrantLock();// 非公平锁// A先到,此时窗口空闲,A获得优先办理的机会,办理业务new Thread(() -> {try {reentrantLock.lock();System.out.println("A is running");try {// A是一个耗时的任务,需要长期占用窗口Thread.sleep(5 * 1000);} catch (InterruptedException e) {e.printStackTrace();}} finally {reentrantLock.unlock();}}, "A").start();// B到了,此时A正在办理业务,只能等待窗口被释放,于是进入AQS等待队列,尝试去抢占窗口new Thread(() -> {try {reentrantLock.lock();System.out.println("B is running");} finally {reentrantLock.unlock();}}, "B").start();// C到了,此时A正在办理业务,只能等待窗口被释放,于是进入AQS等待队列,尝试去抢占窗口,前面是B顾客new Thread(() -> {try {reentrantLock.lock();System.out.println("C is running");} finally {reentrantLock.unlock();}}, "C").start();}
}

lock()

lock()方法是Lock.java接口里的,查看实现类,找到ReentrantLock.javalock()方法,再看实现类,有FairSyncNonFairSync两个实现类。

static final class NonfairSync extends Sync {final void lock() {if (compareAndSetState(0, 1))// 第一个线程抢占setExclusiveOwnerThread(Thread.currentThread());// 设置排他锁拥有者为当前线程else// 第二个线程及后续线程抢占acquire(1);}
}static final class FairSync extends Sync {final void lock() {acquire(1);}
}

acquire()

public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();
}

tryAcquire(arg)

在父类里只提供一个模板,具体实现交由子类来实现。
AbstractQueuedSynchronizer.java

protected boolean tryAcquire(int arg) {throw new UnsupportedOperationException();
}

FairSync.java

protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {// hasQueuedPredecessors()用来判断队列前面是否有元素if (!hasQueuedPredecessors() &&compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0)throw new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;
}

NonFairSync.java会调用nonfairTryAcquire()方法。

final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {// 当前可以抢占// 尝试CASif (compareAndSetState(0, acquires)) {// 抢占成功setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) {// 可重入锁int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;
}

对于tryAcquire()方法,如果抢锁成功了,继续执行下面的流程,如果没有成功,就会进入队列进行排队,也就来到了addWaiter()acquireQueued()方法。

addWaiter(Node.EXCLUSIVE)

private Node addWaiter(Node mode) {Node node = new Node(Thread.currentThread(), mode);// Try the fast path of enq; backup to full enq on failureNode pred = tail;// 初始的时候,tail和head都指向nullif (pred != null) {// 这一段if和enq()里的else的含义是一样的:双向链表的尾插法node.prev = pred;if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}// 加入到排队的等待队列enq(node);// 第一次进这个方法的时候,会直接走enq()方法return node;
}
private Node enq(final Node node) {// 这里可以理解成一个双向链表的插入操作for (;;) {Node t = tail;// 第一次进来if (t == null) { // Must initialize// 创建一个结点,并设置为头结点if (compareAndSetHead(new Node()))tail = head;// 让尾结点也指向这个结点,此时头结点=尾结点,这个结点叫虚拟结点/哨兵结点,用于占位,注意这里的node是new出来的,不是传进来的} else {node.prev = t;// 传进来的结点的prev指向tif (compareAndSetTail(t, node)) {// 尝试将node标记为尾结点t.next = node;// 建立next指向return t;}}}
}

acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

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 GCfailed = false;return interrupted;}// 第一次执行shouldParkAfterFailedAcquire()结果为false,第二次执行shouldParkAfterFailedAcquire()结果为true,此时会继续走parkAndCheckInterrupt()if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {// 如果排队失败或者不想排队了,执行取消操作if (failed)cancelAcquire(node);}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {// 获取前置结点的waitStatusint 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;// waitStatus > 0表示是CANCELED状态if (ws > 0) {/** Predecessor was cancelled. Skip over predecessors and* indicate retry.*/// 循环判断当前结点的前置结点是否也为CANCELED,忽略该状态结点,重新连接队列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.*/// 将前置结点的waitStatus标记为-1,这里意味着有一个结点已经阻塞了,前置结点有唤醒下一个结点的责任compareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;
}
private final boolean parkAndCheckInterrupt() {// 线程挂起,程序不会继续向下执行LockSupport.park(this);// 根据park方法API描述,程序在这三种情况下,会继续向下执行// 1.被unpark// 2.被中断(interrupt)// 3.其他不合逻辑的返回才会继续向下执行// 上述3种情况,会执行到这里,返回当前线程中断状态并清空中断状态// 由于被中断了,该方法返回truereturn Thread.interrupted();
}

unlock()

sync.release(1)tryRelease(arg)unparkSuccessor()
在分析unlock()的时候,还要回看前面的parkAndCheckInterrupt()方法。

public void unlock() {sync.release(1);
}
public final boolean release(int arg) {if (tryRelease(arg)) {Node h = head;if (h != null && h.waitStatus != 0)unparkSuccessor(h);return true;}return false;
}
protected final boolean tryRelease(int releases) {int c = getState() - releases;// 重新计算state的值,可重入锁,计算可重入次数// 当前线程和持有锁的线程不是一个,一般不会出现这个情况if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;if (c == 0) {free = true;setExclusiveOwnerThread(null);// 把排他锁持有者设置为null}setState(c);// 更新state的值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;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.*/// 第一次进来,s为等待队列的第一个结点,且非nullNode 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)// 执行unpack()操作,唤醒s线程LockSupport.unpark(s.thread);
}

看一下异常情况,也就是执行cancelAcquire()方法的情况。
这里要结合双向链表的删除,考虑删除最后一个结点的情况和删除中间结点的情况。

private void cancelAcquire(Node node) {// Ignore if node doesn't existif (node == null)return;// 清空结点里的线程node.thread = null;// Skip cancelled predecessors// 找到前一个结点Node pred = node.prev;// CANCELED为1,大于0,那么继续向前找,直到碰到一个不是CANCELED的结点,一边找一边更新当前结点的前驱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.// 标记当前结点是CANCELEDnode.waitStatus = Node.CANCELLED;// If we are the tail, remove ourselves.if (node == tail && compareAndSetTail(node, pred)) {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;if (next != null && next.waitStatus <= 0)compareAndSetNext(pred, predNext, next);} else {unparkSuccessor(node);}node.next = node; // help GC}
}

http://www.ppmy.cn/news/62069.html

相关文章

IDP中的黄金路径究竟是什么?

在云原生时代&#xff0c;开发人员面临着越来越多的工具、技术、思维方式的选择&#xff0c;给他们带来了极大的认知负担和工作量。为了提高开发人员的开发效率与开发体验&#xff0c;一些头部科技公司开始建立自己的内部开发者平台&#xff08;IDP&#xff09;。在之前的文章我…

阿里云CentOS服务器安装Redis教程(一步步操作)

使用阿里云服务器ECS安装Redis数据库流程&#xff0c;操作系统为CentOS 7.6镜像&#xff0c;在CentOS上安装Redis 4.0.14&#xff0c;云服务器选择的是持久内存型re6p实例&#xff0c;新手站长分享阿里云CentOS服务器安装Redis流程方法&#xff1a; 目录 在CentOS系统中部署R…

C++容器适配器stack和queue(含deque,priority_queue)

目录 1.容器适配器 1.1 什么是适配器 1.2 STL标准库中stack和queue底层结构 1.3 deque 1.3.1 deque原理介绍&#xff08;了解&#xff09; 1.3.2 deque优点和缺点 1.3.3 为什么选择deque作为stack和queue的底层默认容器 2. stack介绍和使用 2.1 stack介绍 2.2 stack使用 2.3 …

删除lpt1.css.asp或com8.index.asp这类文件的方法_asp木马无法删除解决办法

aux|prn|con|nul|com1|com2|com3|com4|com5|com6|com7|com8|com9|lpt1|lpt2|lpt3|lpt4|lpt5|lpt6|lpt7|lpt8|lpt9 但是可以通过cmd的copy命令实现&#xff1a; D:\wwwroot>copy rootkit.asp \\.\D:\wwwroot\lpt6.80sec.asp 前面必须有 \\.\ 已复制 1 个文件。 D…

K8S之自定义Controller

简介 在此之前我们先来了解下kubernetes的两个概念"声明式API"和"控制器模式"。"声明式API"核心原理就是当用户向kubernetes提交了一个API对象的描述后&#xff0c;Kubernetes会负责为你保证整个集群里各项资源的状态&#xff0c;都与你的API对象…

产品经理该怎么催进度?

这算是一个项目管理相关的问题&#xff0c;很多公司会把产品经理与项目经理的工作职能划分并没有这么清晰&#xff0c;而且项目是否能够按时上线&#xff0c;在整个项目推进过程中也是至关重要的。如果是公司的自研产品&#xff0c;项目没办法定期交付&#xff0c;挨老板一顿骂…

K8s基础3——应用部署流程、服务编排、集群资源利用率、日志管理

文章目录 一、应用部署流程二、服务编排2.1 YAML文件格式说明2.2 部署应用2.2.1 命令部署2.2.2 yaml文件部署2.2.2.1 编写deployment.yaml文件2.2.2.2 编写service.yaml文件2.2.2.3 两个yaml文件混用2.2.2.4 测试——service和deployment的标签不一致导致访问网页混乱 2.2.3 自…

腹部肿瘤内科专家朱利明:化疗也能“订制”,晚期结直肠癌不再“无药可救”

肠癌是发生在结肠和直肠的癌症&#xff0c;近二三十年来发病率快速上升。就在近期&#xff0c;“日本女大胃王菅原初代患肠癌病逝”的消息登上热搜&#xff0c;一时引发网友关注热议。 “人生有哲学三问&#xff1a;我是谁&#xff1f;我从哪里来&#xff1f;我到哪里去&#x…