Java并发之AQS浅析

本文深入解析了AbstractQueuedSynchronizer (AQS)的工作原理,包括其内部的CLH锁队列机制,以及如何通过AQS高效构建锁和同步器。AQS作为Java并发包中的核心组件,支撑了ReentrantLock、Semaphore等多个同步类。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

AbstractQueuedSynchronizer(AQS)这个类是许多其他同步类的基类.

AQS是一个用于构建锁和同步器的框架,许多同步器都可以通过AQS很容易并且高效的构造出来.

不仅ReentranLock和Semaphore(信号量)是基于AQS构建的,还包括CountDownLatch,ReentrantReadWriteLock,SynchronousQueue和FutureTask.

基于AQS来构建同步器能带来许多好处,不仅能极大地减少实现工作,而且也不必处理在多个位置上发生的竞争问题.

在基于AQS构建的同步器中,只可能在一个时刻发生阻塞,从而降低上下文切换的开销,并且提高吞吐量.

在设计AQS时充分考虑了可伸缩性,因此java.util.concurrent中所有基于AQS构建的同步器都能获得这个优势.

下面我们来看看AQS内部维护的FIFO队列.CLH

/**
* 等待队列节点类 这个等待队列是一个CLH锁队列变量 CLH锁一般用于自旋锁
* 我们改为使用它们来阻止同步器,并且使用相同的基本策略来保存一些关于其节点的前身中的线程的控制信息。
* 每个节点中的“状态”字段跟踪该线程是否应该阻塞。 一个节点在其前身释放时发出信号,队列中的每个节点反之用作一个具有特定通知的监视器,
* 该监视器监视着一个等待线程,状态字段不会控制线程是否被授予锁等 队列中的第一个线程可能尝试获取锁 ,但是并不能确保成功;
* 它只给了竞争的权利,所以当前发布的竞争者线程可能需要等待。 要进入CLH锁,你将它原子地拼接成新的尾巴。 要出队,你只需设置头部场。

* <pre>
*      +------+  prev +-----+       +-----+
* head |      | <---- |     | <---- |     |  tail
*      +------+       +-----+       +-----+
* </pre>

* 插入CLH队列只需要对“尾”进行单一的原子操作,所以有一个简单的原子点从无排队到排队。类似地,出队只涉及更新“头”。
* 然而,节点需要更多的工作来确定他们的后继者,部分原因是由于超时和中断而可能的取消。
* 头部主要用于去除,如果一个节点被去除,其后继者(通常)将重新链接到未被取消的前身.
* <p>
* 我们还使用“下一个”链接来实现阻塞力学。
* 线程ID或每个节点保存在其自己的节点中,因此前一个信号指示下一个节点,节点通过遍历下一个链接来唤醒,以确定它是哪个线程.
* 确定后继必须避免与新排队的节点进行竞争,以设置其父节点的“下一个”字段.
* <p>
* 等待条件的线程使用相同的节点, 并且使用了一个附件链接. 条件只需要在简单(非并发)链接队列中链接节点,因为它们仅在专门保留时被访问
* 等待时,将一个节点插入到条件队列中,一旦发出信号,节点就被传送到主队列. 一个特殊的状态域将被用来标记节点所在的队列
*/

	/**
	 * AQS会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列
	 */
	static final class Node {
		/** 表明节点正在等待共享模式的标记 */
		static final Node SHARED = new Node();
		
		/** 表明节点正在等待独占模式的标记 */
		static final Node EXCLUSIVE = null;
		
		/** waitStatus值表明线程已取消 */
		static final int CANCELLED = 1;
		
		/** waitStatus值表示后续线程需要运行 */
		static final int SIGNAL = -1;
		
		/** waitStatus值表示当前节点在等待condition,也就是在condition队列中*/
		/** 节点在等待队列中,节点线程在等待Condition,当其他线程对Condition调用了signal()后,该节点将会从等待队列中转移到同步 */
		static final int CONDITION = -2;
		/**
		 * waitStatus值指示下一个acquireShared应无条件地传播
		 */
		static final int PROPAGATE = -3;

		/**
		 * 等待状态,仅接受以下值: 
		 * SIGNAL:
		 * 		该节点的后继者(或将很快被)阻止(通过park),因此当前节点在释放或取消时必须取消其继承者
		 * 		为了避免竞争,获取方法必须首先表明它们需要一个信号,然后重试原子获取,然后在失败时阻塞. 
		 * CANCELLED:
		 * 		该节点由于超时或中断而被取消。 节点永远不会离开这个状态。特别地,具有被取消节点的线程再也不会被阻塞. 
		 * CONDITION:
		 * 		此节点当前处于条件队列. 在传输之前,它不会用作同步队列节点,此时状态将设置为0. PROPAGATE:
		 * releaseShared应该传播到其他节点.
		 * 在doReleaseShared中设置(仅适用于头节点),以确保传播继续,即使其他操作已经干预.
		 *
		 * 这些值以数字排列以简化使用. 非负值意味着节点不需要信号。所以,大多数代码不需要检查特定的值,只是为了符号。
		 *
		 * 对于一般同步节点,该字段被初始化为0,条件节点的CONDITION被初始化 它使用CAS(或可能的话,无条件的易失性写入)进行修改。
		 */
		volatile int waitStatus;

		/**
		 * 前驱节点:
		 * 链接到当前节点/线程依赖于用于检查waitStatus的前导节点。 
		 * 在排队期间分配,并且仅在出队时才被设为null(为了GC)。
		 */
		volatile Node prev;

		/**
		 *  后继节点:
		 * 在排队期间分配,在绕过取消的前辈时进行调整,并在出局时为null排除(为了GC).
		 * 看到一个空的下一个字段不一定意味着该节点在队列的结尾.
		 * 因此,如果下一个字段看起来是空的,我们可以从尾部扫描prev来进行双重检查. 
		 * 被取消节点的下一个字段被设置为指向节点本身而不是null。
		 */
		volatile Node next;

		/**
		 * 启动该节点的线程. 在构造器中初始化,并且使用后置为null
		 */
		volatile Thread thread;

		/**
		 * 链接到下一个节点等待状态,或者特殊值SHARED.
		 * 因为条件队列只能在独占模式下进行访问, 我们只需要一个简单的链接队列来保存节点,当他们在等待条件时. 
		 * 然后将它们转移到队列中以重新获取. 
		 * 并且因为条件只能是排他的,所以我们使用特殊的值来保存一个字段来表示共享模式.
		 */ 
		Node nextWaiter;

同步器拥有三个成员变量:

	/**
	 * 等待队列的头节点,懒加载. 
	 * 除了初始化,它只能通过方法setHead进行修改. 
	 * 注意:如果头存在,则其waitStatus保证不被取消。
	 */
	private transient volatile Node head;

	/**
	 * 等待队列的尾节点,懒加载. 
	 * 仅通过方法enq修改以添加新的等待节点。
	 */
	private transient volatile Node tail;

	/**
	 * 同步状态
	 */
	private volatile int state;
对于锁的获取,请求形成节点,将其挂载在尾部,而锁资源的转移(释放再获取)是从头部开始向后进行。对于同步器维护的状态state,多个线程对其的获取将会产生一个链式的结构。



知道其结构了,我们再看看他的实现。在线程获取锁时会调用AQS的acquire()方法,该方法第一次尝试获取锁如果失败,会将该线程加入到CLH队列中:


	/**
	 * 以独占模式获取,忽略中断。
	 * 至少调用一次{@link #tryAcquire}来实现,成功返回.
	 * 否则线程排队,可能会重复阻塞和解除阻塞, 调用{@link #tryAcquire}直到成功.
	 * 此方法可用于实现方法{@link Lock#lock}.
	 */
	public final void acquire(int arg) {
		if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
			selfInterrupt();	//产生一个中断
	}

下面来看看tryAcquire()方法.

	/**
	 * 尝试以独占模式获取. 该方法应该查询对象的状态是否允许以独占模式获取,如果是,则获取它.
	 * 但是该方法对中断不敏感,也就是说由于线程获取同步状态失败加入到CLH同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移除
	 * <p>
	 * 该方法总是由执行获取的线程调用. 如果此方法报告失败,则获取方法可能会将线程排队(如果尚未排队),直到被其他线程释放为止. 
	 * 这可以用来实现方法 {@link Lock#tryLock()}.
	 * <p>
	 * 默认实现 throws {@link UnsupportedOperationException}.
	 * @param arg
	 *            该值始终是传递给获取方法的值, 或者是进入条件等待时保存的值.
	 */
	protected boolean tryAcquire(int arg) {
		throw new UnsupportedOperationException();
	}
aquireQueue()方法:

	/**
	 * 以不中断的独占模式获取早已在队列中的线程
	 * 当前线程会根据公平性原则来进行阻塞等待(自旋),直到获取锁为止;并且返回当前线程在等待过程中有没有中断过。
	 * acquireQueued方法为一个自旋的过程,也就是说当前线程(Node)进入同步队列后,就会进入一个自旋的过程,
	 * 每个节点都会自省地观察,当条件满足,获取到同步状态后,就可以从这个自旋过程中退出,否则会一直执行下去。
	 */
	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);
		}
	}

如果没有获取到锁,添加到CLH队列.

	/**
	 * 为当前线程和给定模式创建和排队节点。
	 *
	 * @param mode
	 *            Node.EXCLUSIVE用于独占,Node.SHARED用于共享
	 * @return 新节点
	 */
	private Node addWaiter(Node mode) {
		//新建节点
		Node node = new Node(Thread.currentThread(), mode);
		// 快速尝试添加尾节点
		Node pred = tail;
		if (pred != null) {
			//新节点的前驱节点为尾节点
			node.prev = pred;
			//比较并且设置尾节点,如果尾节点与pred相等,则设置node为尾节点
			if (compareAndSetTail(pred, node)) {
				//之前尾节点的下一个节点为新的尾节点
				pred.next = node;
				//返回新的尾节点
				return node;
			}
		}
		//如果pred为空,插入新节点
		enq(node);
		return node;
	}

如果快速设置尾节点失败,则调用enq(Node node);

	/**
	 * 将节点插入队列,如有必要,进行初始化。
	 * 
	 * @param node
	 *            要插入的节点
	 * @return 插入节点的前驱节点
	 */
	private Node enq(final Node node) {
		//构建死循环,多次尝试,直到成功为止
		for (;;) {
			Node t = tail;
			 //tail不存在,设置为首节点
			if (t == null) { // 必须初始化
				if (compareAndSetHead(new Node()))
					tail = head;
			} else {
				//设置为尾节点
				node.prev = t;
				if (compareAndSetTail(t, node)) {
					t.next = node;
					return t;
				}
			}
		}
	}

出列:

当线程是否锁时,需要进行“出列”,出列的主要工作则是唤醒其后继节点(一般来说就是head节点),让所有线程有序地进行下去:


	/**
	 * 以独占模式释放. 通过解除阻塞一个或多个线程实现,如果{@link #tryRelease}返回true。 
	 * 这种方法可以用来实现方法 {@link Lock#unlock}.
	 *
	 * @return 从{@link #tryRelease}返回的值
	 */
	public final boolean release(int arg) {
		if (tryRelease(arg)) {
			Node h = head;
			if (h != null && h.waitStatus != 0)
				unparkSuccessor(h);	//唤醒该节点的后继节点
			return true;
		}
		return false;
	}
tryRelease()方法

	/**
	 * 尝试设置状态以独占模式释放.该方法总是由执行释放的线程调用.
	 * @return {@code true} 
	 * 				如果此对象现在处于完全释放状态,那么任何等待的线程都可能尝试获取; 
	 * @throws UnsupportedOperationException
	 *             如果不支持独占模式
	 */
	protected boolean tryRelease(int arg) {
		throw new UnsupportedOperationException();
	}

unparkSuccessor(Node node)方法:

	/**
	 * 唤醒该节点的后继节点,如果存在。
	 */
	private void unparkSuccessor(Node node) {
		/*
		 * 如果状态为负(即,可能需要信号),则试图在预期信号中清除. 
		 * 如果这样做失败或状态是否被等待线程改变就可以了。
		 */
		int ws = node.waitStatus;
		if (ws < 0)
			//毕竟并且设置状态为0
			compareAndSetWaitStatus(node, ws, 0);
		/*
		 * 断开线程被保存在后台,这通常只是下一个节点. 
		 * 但是,如果取消或显然为空,则从尾部向后移动以找到实际的未取消的后继者。
		 */
		Node 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)
			LockSupport.unpark(s.thread);
	}

取消:

线程因为超时或者中断涉及到取消的操作,如果某个节点被取消了,那个该节点就不会参与到锁竞争当中,它会等待GC回收。

	/**
	 * 取消持续的企图尝试。
	 */
	private void cancelAcquire(Node node) {
		// 如果节点不存在则忽略
		if (node == null)
			return;
		//使该节点的线程为空
		node.thread = null;
		// 跳过取消的先驱节点
		Node pred = node.prev;
		//表明线程已取消
		while (pred.waitStatus > 0)
			//引用指向自己
			node.prev = pred = pred.prev;
		// 前驱节点的下一个节点是明确的节点.以下CAS操作将失败, 
		// 在这种情况下,我们失去了对另一个取消或信号的竞争,所以不需要采取进一步的行动。
		Node predNext = pred.next;
		// 可以使用无条件写而不是CAS。
		// 在这个原子步骤之后,其他节点可以跳过我们。
		// 之前,我们没有其他线程的干扰。
		//将状态设置为已取消
		node.waitStatus = Node.CANCELLED;
		// 如果我们是尾节点,移除自己
		if (node == tail && compareAndSetTail(node, pred)) {
			compareAndSetNext(pred, predNext, null);
		} else {
			// 如果后继者需要信号,请尝试设置pred的下一个链接
			// 所以会得到一个。 否则唤醒它传播。
			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
		}
	}
阻塞:

	/**
	 * 以不中断的独占模式获取早已在CLH队列中的线程
	 * 当前线程会根据公平性原则来进行阻塞等待(自旋),直到获取锁为止;并且返回当前线程在等待过程中有没有中断过。
	 * acquireQueued方法为一个自旋的过程,也就是说当前线程(Node)进入同步队列后,就会进入一个自旋的过程,
	 * 每个节点都会自省地观察,当条件满足,获取到同步状态后,就可以从这个自旋过程中退出,否则会一直执行下去。
	 */
	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);
		}
	}

parkAndCheckInterrupt()方法:

	/**
	 * 方便的方法来阻塞,然后检查是否中断,会把当前线程挂起,从而阻塞住线程的调用栈
	 * 如果中断返回true
	 */
	private final boolean parkAndCheckInterrupt() {
		LockSupport.park(this);
		return Thread.interrupted();
	}
LockSupport.park最终把线程交给系统(Linux)内核进行阻塞。当然也不是马上把请求不到锁的线程进行阻塞,还要检查该线程的状态,比如如果该线程处于Cancel状态则没有必要,具体的检查在shouldParkAfterFailedAcquire中:

	/**
	 * 检查并更新无法获取的节点的状态. 
	 * 如果线程阻塞,则返回true.这是所有采集回路中的主要信号控制. 需要pred == node.prev。
	 *
	 * @param pred
	 *            节点的前驱节点持有状态
	 * @param node
	 *            the node
	 */
	private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
		//节点的前驱节点持有状态
		int ws = pred.waitStatus;
		//如果状态为SIGNAL
		if (ws == Node.SIGNAL)
			// 该节点已经设置了要求发布信号的状态,所以它可以安全地阻塞.
			return true;
		if (ws > 0) {
			 //前驱节点被取消. 跳过前驱节点并指出重试。
			do {
				node.prev = pred = pred.prev;
			} while (pred.waitStatus > 0);
			pred.next = node;
		} else {
			/*
			 * waitStatus必须为0或PROPAGATE. 表明我们需要一个信号,但不要阻塞. 
			 * 来电者需要重试才能确定在阻塞前无法获取。
			 */
			compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
		}
		return false;
	}

参考资料:

实战Java高并发程序设计

java并发编程实战

参考博客:

http://cmsblogs.com/(图片均来自此博客)

源码来自JDK1.8

注释大部分是根据英文注释自己翻译,有不当之处,希望指正

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值