阅读本文前,需要储备的知识点如下,点击链接直接跳转。
由于AQS源码分析篇幅较长,为避免阅读疲劳,特采用系列的形式分成了三篇,建议按顺序阅读。
-
AQS源码分析系列:(一)AQS基础知识 -
AQS源码分析系列:(二)AQS核心:加锁、释放锁、超时中断流程 -
AQS源码分析系列:(三)AQS锁的自定义和实现
本篇主要讲解AQS的基础知识
AQS简介
AQS即AbstractQueuedSynchronizer
的简称,翻译过来就是抽象队列同步器的意思,由Doug Lea大神开发的。说他抽象是因为它提供的是一个基于队列的同步器框架,定义了一些基础功能方法(控制状态变量,获取和释放同步状态方法以及入队出队操作等),具体场景使用只需要根据需要实现对应的方法即可。我们在锁(比如ReentrantLock)、并发工具类(比如CountDownLatch)都可以看到内部类继承了AbstractQueuedSynchronizer
,也就是说AQS才是这些类的基石。说了这么多,感觉把抽象说的越抽象了,下面我们从几个栗子入手吧。
注意:本文使用的JDK版本为JDK8,AQS的代码非常巧妙和经典,很多细节和模块都可以单独拉出来写一篇文章,很多细节问题建议自行阅读和思考。本篇文章主要讲独占模式的应用和原理分析,关于共享模式不再这里展开细讲。
应用举例
ReentrantLock的使用
3个线程获取同一个锁,获得后休眠1秒结束,所以3个线程间隔1秒打印输出。
public class ReentrantLockTest {
public static void main(String[] args) {
lockTest();
}
public static void lockTest() {
ReentrantLock lock = new ReentrantLock();
PrintThread t1 = new PrintThread(lock, "t1");
PrintThread t2 = new PrintThread(lock, "t2");
PrintThread t3 = new PrintThread(lock, "t3");
t1.start();
t2.start();
t3.start();
}
}
class PrintThread extends Thread {
private Lock lock;
public PrintThread(Lock lock, String threadName) {
this.lock = lock;
this.setName(threadName);
}
@Override
public void run() {
lock.lock();
try {
System.out.println(String.format("time:%s,thread:%s,result:%s",
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()),
Thread.currentThread().getName(), "get lock success"));
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
打印结果如下
time:2021-04-13 13:53:55,thread:t1,result:get lock success
time:2021-04-13 13:53:56,thread:t2,result:get lock success
time:2021-04-13 13:53:57,thread:t3,result:get lock success
是因为这3个线程执行时都要先获取锁执行完逻辑后再释放锁,而ReentrantLock
是独占锁,相当于这3个线程间是串行执行的,相互间隔1秒(注意,线程的先后执行顺序不一定是固定的,但线程内有休眠1秒的操作,所以至少相隔1秒)
CountDownLatch的使用
main线程创建一个CountDownLatch latch = new CountDownLatch(1),3个线程持有该CountDownLatch
并调用CountDownLatch
的await()
方法,直到main线程休眠2秒后执行CountDownLatch
的countDown()
方法,释放一个同步状态使得数量值为0,唤醒等待在await()
的线程继续执行。
public class CountDownLatchTest {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(1);
ConcurrentThread concurrentThread1 = new ConcurrentThread(latch, "t1");
ConcurrentThread concurrentThread2 = new ConcurrentThread(latch, "t2");
ConcurrentThread concurrentThread3 = new ConcurrentThread(latch, "t3");
concurrentThread1.start();
concurrentThread2.start();
concurrentThread3.start();
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + " countDown...");
latch.countDown();
}
}
class ConcurrentThread extends Thread {
private CountDownLatch latch;
public ConcurrentThread(CountDownLatch latch, String threadName) {
this.latch = latch;
this.setName(threadName);
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is ready...");
try {
latch.await();
System.out.println(Thread.currentThread().getName() + " is executing...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
打印结果如下(注意,线程的先后执行顺序不一定是固定的)
t1 is ready...
t3 is ready...
t2 is ready...
main countDown...
t1 is executing...
t3 is executing...
t2 is executing...
这三个线程在执行时先打印“…ready”后,然后等待在await()方法上,由于CountDownLatch
是共享锁,而初始的state是1,main线程休眠2秒后调用了countDown()方法会将state置成0,会唤起等待队列里的所有后继线程,所以会相继打印“executing…”。这里就两个简单的使用栗子,不过可以看出,均是在多线程场景中使用,而且代码里并没有AQS相关的影子,那是因为在这些类的内部有内部类去继承了AbstractQueuedSynchronizer
,由这些内部类处理业务逻辑,底层核心逻辑是由AQS框架提供的(线程排队、线程等待、线程唤醒、超时处理、中断处理等),子类调用API实现核心逻辑,AQS在多线程中使用发挥真正的作用。下面我们一步步来分析AQS。
AQS原理分析
类UML图

图中红色连接的线表示内部类,蓝色线表示继承
我们首先来看看AQS相关的URL类图吧,从JDK的源码中我们发现,AQS真正出现的在两个地方,第一个就是lock锁(比如ReentrantLock等),第二个就是并发工具类(比如CountDownLatch、Semaphore等),由这些内部类继承了AQS去实现相关的方法辅助主类实现相关控制,但是我们在JDK的源码中可以看先这些lock锁和并发工具类应在了很多的地方,比如队列、线程池及并发类相关的一些地方。上图把各类的方法展示出来了,我们可以看到继承了AQS类的那些Sync内部类都只用覆盖实现一小部分方法即可完成特定的功能。因为在AQS类中已经实现了大部分底层通用的逻辑,对于其子类来说只用实现部分对外暴露的方法即可,同样我们也可以继承AQS实现自定义的锁或者工具类。
类及方法介绍
AbstractOwnableSynchronizer
public abstract class AbstractOwnableSynchronizer
implements java.io.Serializable {
private transient Thread exclusiveOwnerThread;
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
protected final Thread getExclusiveOwnerThread() {
return exclusiveOwnerThread;
}
}
AbstractOwnableSynchronizer
类里包含一个Thread
的属性并提供了get、set方法,这个Thread对象就是当前持有锁的线程。线程能否支持重入功能就是判断当前线程和持有锁的线程是不是同一个对象,只是同步状态state值增加而已,等线程主动释放锁后该同步状态state值数量值减少。该类使用了abstract
修饰,但是类中并没有抽象方法,目的就是这个类不对外直接使用,而get、set方法使用了protected final修饰,说明方法可被子类使用但不能被子类重写。另外,exclusiveOwnerThread是用了transient
修饰,说明这个属性不参与序列化,因为Thread没有实现Serializable
接口,不能进行序列化处理,另外进程是系统资源分配的最小单位,线程是进程执行的最小单位,线程是由操作系统分配和调度的,所以不能将线程进行序列化。
AbstractQueuedSynchronizer
AbstractQueuedSynchronizer
类也是一个抽象类,继承自AbstractOwnableSynchronizer
,也就拥有了设置持有锁线程的能力,同样该类使用了abstract
修饰,目的就是这个类不对外直接使用,需要具体子类去继承后使用。虽然他实现了序列化接口,但是其内部类Node
并未实现序列化接口,所以在AbstractQueuedSynchronizer
类的属性head、tail都是Node类型并且加了transient
关键字不参与序列化,从以上我们大概就能猜到如果将AQS序列化它只保存一些基本属性的值,并不包含线程以及队列,基本在使用过程中也不会对其进行序列化,具体的属性和队列后续会详细介绍,下面列举一些AQS类里重要的方法和属性。
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
/**
* 独占模式,尝试获取同步状态,立即返回获取成功或失败,需要子类实现
*/
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
/**
* 独占模式,尝试释放同步状态,立即返回获取成功或失败,需要子类实现
*/
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
/**
* 共享模式,尝试获取共享锁,需要子类实现,
* 立即返回获取的数量值
* 0:获取锁成功,没有剩余资源
* > 0:获取锁成功,并且有剩余资源
* < 0:获取失败
*/
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
/**
* 共享模式,尝试释放共享锁,需要子类实现,释放成功返回true
*/
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
/**
* 当前线程是否独占资源,需要子类实现,true:是,false:否
*/
protected boolean isHeldExclusively() {
throw new UnsupportedOperationException();
}
/**
* 入队
*/
private Node enq(final Node node) {...}
/**
* 将当前线程封装成Node逻辑里也有调入队enq方法的逻辑
*/
private Node addWaiter(Node mode){...}
/**
* 【重要】对外提供的获取锁的方法,子类调用此方法执行获取锁的动作,
* 内部调用包含了获取锁、排队、阻塞、中断等操作
*/
public final void acquire(int arg) {...}
/**
* 【重要】对外提供的释放锁方法,子类调用此方法执行释放锁的动作,
* 内部包含更新state、唤醒等待队列的第一个等待节点
*/
public final boolean release(int arg) {...}
/**
* 【重要】双向队列头结点
*/
private transient volatile Node head;
/**
* 【重要】双向队列尾结点
*/
private transient volatile Node tail;
/**
* 【重要】同步状态,控制线程是否可获取资源,是用一个整型的变量表示,
* 加了volatile,保证了该变量在多线程间的可见性
*/
private volatile int state;
/**
* 静态内部类,将等待锁的线程封装成Node进行排队
*/
static final class Node {
...
}
// 其他方法、属性、内部类未列出
...
}
该类中没有抽象方法,但是上面提到的几个方法都是抛了UnsupportedOperationException
异常,说明需要具体子类实现时去复写,这也正是独占模式和共享模式要对应实现的方法。head、tail两个Node
类型的属性分别表示了双向链表的队头和队尾,如果线程不能获取到锁则进入队列排队并且等待唤醒或者超时中断,后续细讲。整型的state属性比较核心,表示同步状态,就是用它来控制线程是否需要阻塞。上面的代码没有列出其他方法,部分方法源码后文会详细分析。
Node类
AQS类中有一个非常重要的内部类Node
,我们称作它为节点,这个内部类是AQS框架线程排队的基石,非常核心,按照注释上所说Node类是CLH
队列的一种变种(CLH队列是一种单向队列,这里不做介绍,感兴趣可自行搜索),Node类是一种双向队列,内部有Node prev,Node next属性,分别表示前驱节点和后继节点,还有一个Thread
属性,表示封装的当前线程,所以AQS的队列其实就是以Node节点形成的一个双向链表,结构如下:我们看下Node类的属性和方法类图。
-
节点模式:Node SHARED = new Node()来表示共享模式,Node EXCLUSIVE = null表示独占模式。 -
节点等待状态waitStatus:这个属性字段比较重要,因为它是AQS控制线程执行的关键字段,这个值的改变是采用CAS操作的。他的取值只有以下几种。(1)1:CANCELLED,取消状态,可能情况有节点等待超时被取消或者被中断,那么代表这个Node节点中包含的线程未获取到锁,由具体业务判断是否需要执行后续逻辑。(2)0:初始化值,创建节点的时候默认会初始化,0也就是他的默认值。(3)-1:SIGNAL,表明该节点以后的线程需要等待唤醒,后续节点的线程可以阻塞。(4)-2:CONDITION,表明该节点的线程需要等待,由 ConditionObject
实现条件队列会用到。(5)-3:PROPAGATE,一般在共享模式下会有该状态,表明头节点获取到了共享资源,可向后传播,等待队列里的其他节点也都可以获取共享资源。 -
Thread thread属性对象 AQS框架将当前正在获取同步状态的线程包装成Node节点的一个属性,根据Node节点的waitStatus状态来控制当前线程是被唤醒继续尝试获取锁还是线程取消。
队列
AQS内部的两个变量head代表队列的头节点,tail代表队列的尾节点,是一个双向队列,如Node类所介绍,head和tail指向如下图所示。注意:head节点比较特殊,队列里需要唤醒的线程是从head节点的next节点开始, 在队列初始化时放的是一个new Node()对象,属性thread并没有赋值,后续排队的线程被唤醒时会把他自己设置成head并且将thread属性设置成null。所以head节点可以这么理解,head节点初始化时是一个虚拟节点,没有用处,只是充当一个队头标识,当队列中有线程排队时,说明head节点已经是获取到锁的线程的节点了,等这个线程执行完需要唤醒head.next之后的线程继续执行,这就是排队和唤醒的逻辑。
同步状态
在AQS类中,有一个state属性,描述如下
/**
* The synchronization state.
*/
private volatile int state;
state是整型变量,叫同步状态,也可叫加锁的次数,使用了volatile修饰,保证了线程间的可见性,所有的线程是否可获取到锁资源都是基于对这个字段值的操作来确定。对于独占锁来说,初始情况下state=0,表示当前资源空闲,可被线程获取到锁,如果state>0,表示已经有线程占用资源,后续的线程(非持有锁的线程)需要进入队列,不会存在<0的情况,因为如果释放锁的过程中到state=0时就已将exclusiveOwnerThread置成null了,所以多次调用释放锁的方法时,如果exclusiveOwnerThread不是当前线程的话,则会抛出IllegalMonitorStateException
异常。
公平锁&非公平锁
-
公平锁:
多个线程获取锁时按照请求的先后顺序排队,不存在插队的情况。常用的实现方式如下:
final void lock() {
acquire(1);
}
acquire方法是AQS的获取锁方法,多线程竞争获取锁时会排队。
-
非公平锁:
多个线程获取锁时,首先不是按照请求的先后顺序排队,而且先尝试去获取锁,也就是抢占式获取,如果获取到了那么该线程就是持有锁的线程可以执行他的逻辑,如果没有获取到锁,那么就会走入队排队流程,所以有可能会出现后到的线程可能比等待队列里的线程先获取到锁。常用的实现方式如下:
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
通过代码可以看到非公平的情况下,线程会先尝试使用cas方式设置state,如果设置成功则获取到锁,设置失败则走入队排队等待获取锁流程。所以,这两个的区别在于是否会抢占获取锁。设置成公平锁时,每个线程获取锁的概率是一样的,每个线程会先看等待队列是否为空,若为空,直接获取锁,若不为空,自动排队等候获取锁;设置成非公平锁时,所有的线程都会优先去尝试争抢锁,不会按顺序等待,若抢不到锁,再用类似公平锁的方式获取锁。那为什么会这样设计呢,这两种分别使用在什么场景下呢。
-
恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU的时间片,尽量减少CPU空闲状态时间 -
使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销 貌似上面说的两点都是非公平锁比较好,但是非公平锁也有他的问题,有可能导致排队的线程长时间排队也没有机会获取到锁,这就是传说中的“锁饥饿”,如果使用的是带有超时时间的方式获取锁,则可能导致排队中的线程大面积超时获取锁失败。那什么时候用公平锁,什么时候用非公平锁?如果为了更高的吞吐量,非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了; 否则那就用公平锁,大家按请求先后顺序排队使用。 欢迎关注公众号,欢迎分享、点赞、在看
原文始发于微信公众号(小新成长之路):AQS源码分析系列:(一)AQS基础知识
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/238544.html