通过一个生活中的案例场景,揭开并发包底层AQS的神秘面纱

Posted Java爱好者社区

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了通过一个生活中的案例场景,揭开并发包底层AQS的神秘面纱相关的知识,希望对你有一定的参考价值。

本文导读

  • 生活中案例场景介绍
  • 联想到 AQS 到底是什么
  • AQS 的设计初衷
  • 揭秘 AQS 底层实现
  • 最后的总结

当你在学习某一个技能的时候,是否曾有过这样的感觉,就是同一个技能点学完了之后,过了一段时间,如果你没有任何总结,或者是不经常回顾,遗忘的速度是非常之快的。

忘记了之后,然后再重新学,因为已经间隔了一段时间,再次学习又当做了新的知识点来学。这种状态如此反复,浪费了相同的时间,但学习效果却收效甚微。

每当遇到这种情况,我们可以停下来,思考一下。对于某一个技术知识点理解起来不是那么好懂的时候,或者是学习起来有点吃力的时候,咱们可以尝试找找生活中的例子来联想下。

因为技术源于生活。

找到一个合适的生活案例,然后结合你自己做笔记总结和动手实践的过程。定期的去回顾一下,慢慢的就会理解的更加透彻。

1、生活中案例场景介绍

今天我们就举一个生活中的例子来理解下并发底层的AQS。

大家如果去过某些大医院的话,就能知道,由于互联网的快速发展,医院的挂号、交费、取药的流程都是比较方便的,交费也可以使用支付宝、微信支付了,而不用带现金了。

医生开完单子,交费完成 ,单子上都会有一个长条二维码,可以直接在取药的地方自助扫码,叫号系统自动分配取药窗口,然后你在关注下指定窗口等待着叫号就可以了,叫到你的时候再过去取药,而不需要一直在等待着。

我们用一张图来直观的感受下:

file

这里面涉及到了几个角色:

1)药房,提供取药窗口的,内部有自助取药机或人工取药

2)取药叫号系统,当用户扫码药单后,自动录入到该系统中

3)取药用户

接下来咱们细化下取药流程。

当取药用户在自助机器上扫码时,可以直观的看下下面的流程图:

取药流程图1

第一个用户是程序猿,因为有多个自助扫码机,他一看二维码就知道咋回事了,所以第一个在自助机上扫码完成,可以优先第一个去取药窗口(State窗口)。

此时叫号系统的药单队列中还没有其他人,程序猿扫码后,就可以直接去窗口等待着取药了。

接下来,本来是张大爷和王大妈看着先前程序猿的操作,也跟着在自助机上来回扫码一把,由于不大懂扫哪里,扫了半天也没有个反应,老头此时有点懵 : (。

后来热心的程序猿看到了,给指点了一下 : ),帮助顺利的扫码完成。

再看下面这个流程图:

取药流程图2

正好,张大爷和王大妈的取药单,也被分配到跟程序猿同一个取药窗口中 ,此时只能排队了,按照他们的扫码顺序排队,如上图所示。

当程序猿取药完成,叫号系统会自动呼叫下一位用户,即队列中的排在首节点的张大爷,自助取药机收到消息会自动给张大爷取药。此时,王大妈还是要等一会。后面的用户 CCC 扫码完成后,会继续放到药单队列中,药单队列是按照 FIFO,也就是谁先扫码谁就在前面,所以 CCC 排在王大妈的后面。

再看下面的流程图:

取药流程图3

张大爷还在等待取药过程中,王大妈也知道下一个可能就是她了,所以王大妈会时不时的,抬头看看叫号窗口是否显示了自己的名字。
此时,王大妈可以稍微在等待区休息一会,等待系统叫号就可以了。

2、联想到 AQS 到底是什么

其实,上面的场景介绍中,在医院里是很常见的。那么这个场景对应的,我们可以联想到 Java 中的并发编程。

如果没有中间的叫号系统来做控制,如果医院没有限制,很多用户要么一拥而上没有秩序的乱挤,要么就有秩序的都在窗口站着排成长队等待着。

所以中间的叫号系统解决了很多问题,解决了很多取药用户的有序性、安全性,而且不需要用户一直等着,用户线程无阻塞,当收到系统通知信号后,用户再继续执行取药动作。

这个生活中的例子,可以很好的联想到 Java 中我们常用的,并发包的底层技术:AQS (AbstractQueuedSynchronizer)队列同步器(简称同步器)。

就像我们举得例子中的提到的几个角色,有很多用户(理解为用户线程),有共享资源(取药窗口)。在用户线程和共享资源之间,是通过中间系统来协调控制的,这里面就会涉及的概念。

是用来控制多个线程访问共享资源的方式。一个锁能防止多个线程对共享资源的同时访问,有些锁也允许多个线程并发访问共享资源,比如读写锁。

在 Java 中经常使用的锁是 synchronized,synchronized 会隐式的获得锁,但它必须是先获得锁再释放锁。这种方式简化了同步的管理,但扩展性不如 Lock 显示的获得锁和释放锁更加灵活。

synchronized 和 Lock 锁之间的区别:

synchronized和Lock锁区别

从性能上来讲,当并发量高、竞争激烈的场景下,Lock 锁会较 synchronized 性能上表现的
更稳定些。反之,当并发量不高的情况下,synchronized 有分级锁的优势,因此两者性能差不多,synchronized 相对来说使用上更加简单,不用考虑手工释放锁。

直观感受下两者的性能对比:

性能对比

Lock 显示的锁使用,因为使用上更加灵活,这得益于其底层基础同步框架的实现机制,它就是 AQS。

如下图所示:

多线程访问共享资源

上述图中列出了多个并发包中的类,每一个并发工具类解决的问题场景不同,但是其底层同步框架基本都是使用的 AQS 来实现的。

3、AQS 的设计初衷

Java 大佬考虑并发底层使用 AQS 的设计思想初衷,就是为了能够抽象出来统一的同步协调处理器,设计好顶层结构,作为并发包构建的基本骨架,该骨架里封装了多线程的入队/出队、线程阻塞/唤醒等一系列复杂的操作。Java SDK 中面向开发者针对不同需求场景提供了多个并发包工具。

尽管,提供的这些并发包的实现方式是不一样的,但都是基于顶层抽象出来的 AQS 所定义的统一接口基础上,然后部分定制逻辑延迟到子类去自行实现。同时,部分定义的方法中是按照既定的顺序执行的,由此,我们也能够想到,AQS 使用了模板方法模式。

在上一节图中提到的几个并发包中,我们来简单介绍下实现场景。

多线程独占式并发工具:

1)ReentrantLock

可重入锁,同一时刻仅允许一个线程访问,所以可以称作 独占锁,线程可以重复获取同一把锁。

多线程共享式并发工具:

1)ReentrantReadWriteLock

可重入的读写锁,允许多个读线程同时进行,但不允许写-读、写-写线程同时访问。

适用于读多写少的场景下。

2)CountDownLatch

主要用来解决一个线程等待 N 个线程的场景。

就像短跑运动员比赛,等到所有运动员全部都跑完才算竞赛结束。

3)CycliBarrier

主要用于 N 个线程之间互相等待。

就像几个驴友约好爬山,要等待所有驴友都到齐后才能统一出发。

4)Semaphore

限流场景使用,限定最多允许N个线程可以访问某些资源。

就像车辆行驶到路口,必须要看红绿灯指示,要等到绿灯才能通行。

基于上述这些并发包工具,我们可以根据多线程的不同使用场景去选择。JDK 提供的这些并发包基本能够满足了大部分的开发者的使用需求。

4、揭秘 AQS 底层实现

在用户取药的这个例子中,我们可以把多个用户扫码取药行为,联想为多线程共用争抢一个窗口的锁,窗口就作为共享资源来看待。所以,哪个用户先扫码,这个用户就优先有机会能提前取药。

对应联想到 AQS 内部结构,如下图所示:

AQS内部结构模拟图

我们根据用户取药的流程,对应画出来的一个 AQS 底层的大致结构图。经过举例分析,多个用户(线程)扫码取药会争抢一把锁(同一个取药窗口,共享资源),所以用 Java 并发包里的 ReentrantLock 锁的使用来描绘一下也更加贴切,因为 ReentrantLock 是一个独占锁,同一个时刻只允许一个用户执行。

结构图中的 AQS 里,包含了几个关键的属性:

  • state 变量:表示同步状态
  • exclusiveOwnerThread 变量:表示当前加锁的线程
  • Node:CLH 队列,是一个 FIFO 的双端双向链表队列

啥是CLH?在 AQS 源码中你能找到一段话,The wait queue is a variant of a "CLH" (Craig, Landin, and Hagersten) lock queue,看上去像是三个人的名字,他们来发明的自旋算法,没具体查资料。

AQS 队列同步器主要包括:

  • 独占式同步状态获取和释放,如:ReentrantLock
  • 共享式同步状态获取和释放,如:Semaphore、CountDownLatch、CycliBarrier

接下来,我们就用独占式 ReentrantLock 可重入锁来分析下 AQS 底层到底了做了哪些事情。

使用 ReentrantLock 显示加锁解锁代码很简单,如下所示:

Lock lock = new ReentrantLock();
lock.lock();

// doSomething...

lock.unlock();

先来一张类图:

类图

列出了 Lock 接口和 ReentrantLock 实现类里的核心方法,其中 ReentrantLock 里的有个非常核心的属性是 Sync ,它才是最最关键的组件,继承了 AbstractQueuedSynchronizer 抽象类,作为子类实现了加锁和解锁。

再看一张全景类图:

file

这张类图中列出了 ReentrantLock 类里的 Sync 及其两个子类 FairSync 公平锁 和 NonfairSync 非公平锁的核心方法,AQS 类里的核心属性和方法。

AQS 中的 Node同步队列关键属性介绍:

waitStatus 等待状态:

CANCELLED:值为1,等待的线程等待超时或被中断,需从同步队列中取消等待,节点进入该状态不在变化。

SIGNAL:值为 -1,后继节点的状态处于等待状态,而当前节点线程如果释放了同步状态或被取消,将会通知后继节点,使得后继节点的线程得以运行。

CONDITION:值为 -2,节点在等待队列中,节点线程等待在 Condition 上,当其他线程对 Condition 调用了 signal() 方法后,该节点将会从等待队列转移到同步队列中,获取同步状态。

PROPAGATE:值为 -3,表示下一次共享式同步状态获取将会无条件的被传播下去。

INITAL:值为 0,初始状态,当你创建新的节点时,默认就是这个状态值。

双向双端队列:

在 AQS 结构图中已经有所描述,Node 是一个双端双向链表的队列,双端表示有 head (头节点)和 tail(尾节点)。

双向链表表示有 prev (指向前驱节点)和 next (指向后继节点)两个指针来标识 ,在上述 AbstractQueuedSynchronizer.Node 类图中也能够看得到。
此外,Node 中还有 thread 属性表示当前的线程。

介绍完了类图中的关键属性和数据结构,我们来分析下,ReentrantLock 对象调用了 lock() 方法加锁的过程。

找到 ReentrantLock 类里的 lock() 方法如下:

public void lock() {
		sync.lock();
}

看到没,sync 变量就是 Sync 刚提到的 AQS 的子类,调用了 sync 的 lock() 方法。

当我们点击进去 sync#lock() 方法时,发现是个抽象方法,可以找到两个实现类,如下所示:

lock方法

此时,如果不经常看源码的同学,可能有点懵,到底是走那个方法?一种方式,你可以在 NonfairSync 和 FairSync 两个类的 lock() 方法上都打上断点,直接调试看到底是哪个类;另外,你可以猜测下,这个实现类应该是在对象初始化时创建的,所以你就直接去找构造方法。

public ReentrantLock() {
		sync = new NonfairSync();
}

我们是通过默认构造方法创建的 ReentrantLock,跟进去看到的是创建的 NonfairSync,即默认创建的是非公平锁方式。

看下 NonfairSync#lock() 方法实现:

final void lock() {
	if (compareAndSetState(0, 1))
			setExclusiveOwnerThread(Thread.currentThread());
	else
			acquire(1);
}

有两条执行路径:

1)直接通过 compareAndSetState(0, 1) 方法,使用了 CAS 可以无锁化的保证一个数值修改的原子性,判断下如果 state 变量是 0,说明没有线程加锁,可以把 state 设置为 1。设置成功后,

调用 setExclusiveOwnerThread(Thread.currentThread()) 方法,将当前线程设置为加锁线程,即将 exclusiveOwnerThread 变量赋值为当前线程。

protected final boolean compareAndSetState(int expect, int update) {
		// See below for intrinsics setup to support this
		return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

compareAndSetState(int expect, int update) 底层调用了 UnSafe 类的 compareAndSwapInt(this, stateOffset, expect, update) 方法,该方法为 JDK 内部使用的API,进行的是指针操作,基于 CPU 指令实现的原子性的 CAS。

图示如下:

线程1获得锁

2)如果 state 变量不是 0,说明有线程已经加锁了,compareAndSetState(0, 1) 方法返回 false,执行 acquire(1) 方法。

当我们点击 acquire(1) 方法后,就进入到了 AbstractQueuedSynchronizer 类里面了。

acquire(int arg) 方法源码:

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

其他线程加入同步队列,图示如下:

线程加入AQS同步队列

上述代码完成如下几个步骤:

1)首先调用 tryAcquire(int arg) 方法,保证线程安全的获取同步状态,如果同步状态获取失败,进入步骤2)。

2)调用 addWaiter(Node node) 方法,参数为构建的独占式 Node.EXCLUSIVE 节点,将构建好的节点通过 CAS 无锁化方式添加到同步队列的尾部,并返回该节点。

3)最后调用 acquireQueued(Node node, int arg) 方法,使得该节点按「死循环」方式获取同步状态。如果节点获取不到同步状态,则会调用 LockSupport#park() 方法挂起,阻塞节点中的线程,被阻塞的线程等待唤醒,唤醒方式主要是前驱节点出队或被中断来实现的。

下面结合源码具体剖析下上述的几个步骤。

当调用 tryAcquire(int arg) 方法,注意 AQS 里的 方法是这样的:

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

1)tryAcquire(int arg) 尝试获取同步状态分析:

这就是 AQS 提供的模板方法,由于子类自定义同步器去实现的。

所以,会跳转到 NonfairSync 里的 tryAcquire(int arg) 方法:

protected final boolean tryAcquire(int acquires) {
		return nonfairTryAcquire(acquires);
}

内部调用了 nonfairTryAcquire(int acquires) 方法,该方法是 Sync 父类的,如下所示:

final boolean nonfairTryAcquire(int acquires) {
  // 获取当前线程
	final Thread current = Thread.currentThread();
  //  获取同步状态
	int c = getState();
	// 如果同步状态是0,没人加锁
	if (c == 0) {
	  // 通过CAS方式设置同步状态,尝试将0修改为1
		if (compareAndSetState(0, acquires)) {
				// 设置当前加锁的线程,给exclusiveOwnerThread变量赋值
				setExclusiveOwnerThread(current);
				return true;
		}
	}
	// 当前线程等于当前加锁线程
	else if (current == getExclusiveOwnerThread()) {
		// 计算新的同步状态值,nextc = 1 + 1 = 2
		int nextc = c + acquires;
		// 判断下nextc,防止溢出
		if (nextc < 0) // overflow
				throw new Error("Maximum lock count exceeded");
		// 更新同步状态值
		setState(nextc);
		return true;
	}
	return false;
}

即使是多线程访问,同一时刻总是仅有一个线程能够获得同步状态,就会走上述的 c == 0 里的逻辑。

如果是在同一个线程中进行了第二次调用 ReentrantLock#lock() 和 unlock() 方法呢?此时 c = 1,所以会走到 current == getExclusiveOwnerThread() 判断当前线程是等于加锁线程的,那么就会计算 nextc 新的同步状态 ,如果该值不会溢出,则调用 setState(int newState) 更新同步状态值,state 同步状态值变为 2。

** 2)addWaiter(Node node) 添加到同步队列分析:**

addWaiter(Node node) 主要是将节点加入到同步队列队尾,源码如下所示:

private Node addWaiter(Node mode) {
  // mode传进来的参数为Node.EXCLUSIVE,构建Node节点
	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;
			// 1. 将当前节点作为尾节点添加到同步队列
			// 2. 原尾节点作为当前节点的前驱节点
			if (compareAndSetTail(pred, node)) {
					pred.next = node;
					return node;
			}
	}
	// 同步队列为空,调用enq(node)方法
	enq(node);
	return node;
}

继续看 enq(Node node) 方法源码:

private Node enq(final Node node) {
	for (;;) {
		Node t = tail;
		// 第一次循环,尾节点为空
		if (t == null) { // Must initialize
				// 创建空Node节点作为Head头节点
				if (compareAndSetHead(new Node()))
						tail = head;
		} else {
			 // 第二次循环过来,只有一个节点,就是头结点
				node.prev = t;
				// 将当前节点作为尾节点添加到同步队列中
				if (compareAndSetTail(t, node)) {
					 // 当前节点作为头结点的后继节点
						t.next = node;
						return t;
				}
		}
	}
}

也是使用了 CAS 无锁化保证节点,可以正确的添加到同步队列中。

第一次循环,尾节点为空,调用了 compareAndSetHead(new Node()) 方法,底层调用了 unsafe.compareAndSwapObject(this, headOffset, null, update) 如果 head 变量所在位置为 null,则更新为空 Node 节点。

第二次循环,尾节点不空,调用了 compareAndSetTail(t, node) 方法,底层调用了 unsafe.compareAndSwapObject(this, tailOffset, expect, update) ,此时 tail 变量所在位置为空 Node 节点,更新为当前节点,即 Node.EXCLUSIVE 独占式节点。

** 3)acquireQueued(Node node, int arg) 获得同步状态分析:**

节点加入到同步队列后,就进入到了自旋的过程,每个节点都在不断的观察,是否可以获得同步状态,成功获得同步状态,就会从这个自旋过程中退出。如下所示是自旋过程的实现代码。

acquireQueued() 方法源码如下:

final boolean acquireQueued(final Node node, int arg) {
	boolean failed = true;
	try {
		boolean interrupted = false;
		for (;;) {
				// 获得当前节点的前驱节点
				final Node p = node.predecessor();
				// 如果p是头节点,则尝试获得同步状态
				if (p == head && tryAcquire(arg)) {
						// 成功获得同步状态,把自己作为Head头节点
						setHead(node);
						// 原头节点从同步队列移除,不需要CAS操作
						p.next = null; // help GC
						failed = false;
						return interrupted;
				}
				// 1. 如果不是头节点,失败获得同步状态,判断下是否可以挂起
				// 2. 允许挂起 ,调用 LockSupport#park() 方法完成线程挂起,释放锁
				if (shouldParkAfterFailedAcquire(p, node) &&
						parkAndCheckInterrupt())
						interrupted = true;
		}
	} finally {
		if (failed)
				cancelAcquire(node);
	}
}

当前线程挂起过程,先调用 shouldParkAfterFailedAcquire(Node pred, Node node) 方法,如下所示:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
	int ws = pred.waitStatus;
	// 前驱节点状态为SIGNAL,返回true
	if (ws == Node.SIGNAL)
			return true;
	if (ws > 0) {
	    // 跳过 CACALLED 状态的节点
			do {
					node.prev = pred = pred.prev;
			} while (pred.waitStatus > 0);
			pred.next = node;
	} else {
			// 前驱节点的状态小于0,则更新为SIGNAL状态
			compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
	}
	return false;
}

图示如下:

AQS同步队列节点自旋过程

线程1首先获得了同步状态,线程2、线程3发现 AQS 类里的 state 不为 0,所以都被添加到 AQS 的同步队列尾部。

此时,同步队列中的线程2和线程3的节点会进行自旋过程,线程2的前驱节点是头节点,满足这个条件,然后调用 tryAcquire(int arg) 方法尝试获得同步状态。

当线程1业务处理完成,需要释放同步状态,是的后续节点线程能够获得同步状态。示例中会使用 ReentrantLock#unlock() 方法来解锁。

继续来分析 unlock() 方法,如下代码所示:

public void unlock() {
	sync.release(1);
}

在 unlock() 方法中,调用的 Sync 类的 release(int arg),进入到该方法中。

public final boolean release(int arg) {
  // 释放同步状态
	if (tryRelease(arg)) {
			Node h = head;
			if (h != null && h.waitStatus != 0)
			    // 唤醒后继节点
					unparkSuccessor(h);
			return true;
	}
	return false;
}

这个 release(int arg) 是在 AQS 类里的了,其内部会调用 tryRelease(int arg) 方法尝试释放同步状态,如果成功释放,获得同步队列里的 head 头节点,头节点不为空并且它的 waitStatus 状态不为 0(即不为 INITAL 初始状态),则会调用 unparkSuccessor(Node node) 唤醒后续节点。

当直接点击进入 tryRelease(int arg) 方法,还是在 AQS 类里,如下所示:

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

AQS 类的该方法并没有提供实现,跟 tryAcquire(int arg) 方法类似的,会由 Sync子类里的 tryRelease(int arg) 重写该方法实现,如下所示:

protected final boolean tryRelease(int releases) {
	// 获得同步状态为1,releases为1,所以c计算得到0
	int c = getState() - releases;
	// 当前线程不是加锁线程,则抛出IllegalMonitorStateException异常
	if (Thread.currentThread() != getExclusiveOwnerThread())
			throw new IllegalMonitorStateException();
	boolean free = false;
	if (c == 0) {
			free = true;
			// 将加锁线程变量设置为null
			setExclusiveOwnerThread(null);
	}
	// 将state变量更新为计算得到的0,即更新同步状态
	setState(c);
	return free;
}

如果释放同步状态成功,上述方法将会返回 true。完成的事情很简单,就是将 state 变量的同步状态更新一下,然后将加锁线程 exclusiveOwnerThread 变量设置为 null。

然后,调用 unparkSuccessor(Node node) 方法通知后继节点,源码如下:

private void unparkSuccessor(Node node) {
	int ws = node.waitStatus;
  // 等待状态小于0,则通过CAS更新等待状态值为0
	if (ws < 0)
			compareAndSetWaitStatus(node, ws, 0);
  // 获得头节点的后继节点,即线程2
	Node s = node.next;
	// 如果后继节点等待状态大于0,说明是CACELLED失效节点
	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);
}

图示如下:

线程1释放锁

通过图示并结合源码,相信大家理解起来就更加清晰了。

注意,线程1释放同步状态后,会通知 后继节点是线程2,不是 Head 头节点。

上述图中,同步队列中的线程2被唤醒后,我们回到 acquireQueued(final Node node, int arg) 这个节点自旋过程的源码看下。可以在上面找一下这个方法的源码,其中线程2调用了 parkAndCheckInterrupt() 方法将线程挂起着,如下所示:

private final boolean parkAndCheckInterrupt() {
		LockSupport.park(this);
		return Thread.interrupted();
}

唤醒之后,继续执行,调用 Thread.interrupted() 方法检测下当前线程中断情况。如果没有被中断,则继续循环,执行如下代码:

final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
		setHead(node);
		p.next = null; // help GC
		failed = false;
		return interrupted;
}

node 变量为线程2,调用 p = node.predecessor() 方法获得前驱节点为头节点,满足 p == head 条件,然后调用 tryAcquire(int arg) 尝试获得同步状态,经过上述分析,因为 state 为 0,说明没有线程加锁,所以获得同步状态成功,该方法返回 true。

调用 setHead(node) 方法,如下所示:

private void setHead(Node node) {
		head = node;
		node.thread = null;
		node.prev = null;
}

将 node 作为头节点,node 的 prev 前驱节点指针和 thread 线程变量设置为 null。

图示如下所示:

AQS同步队列节点唤醒

上述图中,看到原来的头节点,已经没有任何引用了,将来会被 JVM 垃圾回收掉。

刚刚被唤醒的线程2当做了头节点,但实际也是个空节点了, 因为该节点的 thread 设置为 null了。此时,线程3的节点还在自旋状态,等线程2释放锁后,通知后继节点,唤醒线程3。都会执行我们上面分析的同一个套路。

最后,经过对上述源码和图示的分析,咱们来两张完整的流程图,方便大家记忆。

ReentrantLock#lock() 方法获得锁流程图:

ReentrantLock#lock()获得锁流程图

ReentrantLock#unlock() 方法释放锁流程图:

ReentrantLock#unlock()释放锁流程图

**5、最后的总结 **

本文以生活案例场景(医院窗口取药流程)介绍为例,联想到 AQS 到底是什么,接着介绍对 AQS 设计初衷, 并且以 ReentrantLock 独占式锁为例,深入剖析了 AQS 底层数据结构,以及源码的实现细节。

AQS 是 Java 并发包中很多同步组件的构建基石,它内部主要是由同步状态 state 变量和一个 CLH 同步 FIFO 队列协作来完成的,CLH是一个双端双向链表数据结构。

当新的线程节点无法获得同步状态,将会加入到同步队列队尾,此时会采用 CAS 无锁化来确保该操作的线程安全,保证原子性。线程加入到同步队列后会被挂起,等待释放锁唤醒后继节点,使得继续获得同步状态。

AQS 采用了模板方法设计模式,根据不同并发包组件同步需求场景,子类同步器只需重写 tryAcquire(),tryAcquireShared(),tryRelease(),tryReleaseShared() 几个方法来决定同步状态的获取和释放,tryAcquire() 和 tryRelease() 方法同于独占式,tryAcquireShared() 和 tryReleaseShared() 用于共享式。

对于 Java 中很多并发包背后复杂的入队/出队,线程阻塞/唤醒,线程安全的保证等,全部都由 AQS 来帮助你完成了,Doug Lea 大神很是牛逼呀!

弄懂了 AQS,大部分并发包里的工具类都是很容易理解了。另外,对于共享式并发包的源码,大家如果感兴趣,可以借助本文的源码分析过程,去自行画图分析一下。

希望本文给大家能带来一点点的帮助!能够抵挡得住面试官的N个连环炮式发问。

欢迎关注我的公众号,扫二维码关注获得更多精彩文章,与你一同成长~
Java爱好者社区

以上是关于通过一个生活中的案例场景,揭开并发包底层AQS的神秘面纱的主要内容,如果未能解决你的问题,请参考以下文章

AQS:Java 中悲观锁的底层实现机制

AQS底层原理分析

多线程 AQS底层原理分析

多线程 AQS底层原理分析

深入java并发包源码AQS的介绍与使用

J.U.C之AQS介绍