深入理解Java多线程AQS及其底层实现

首先,为什么要理解AQS???
因为同步组件(这里不仅仅指锁,还包括CountDownLatch等)的实现依赖于同步器AQS,即AQS是同步组件实现的核心部分
那么,AQS到底是什么呢???
AQS(AbstractQueuedSynchronizer),简称同步器,是用来构建锁和其它同步组件的基础框架。AQS的组成可以理解如下图:
在这里插入图片描述要想掌握AQS的底层实现,我们就要学习这些模板方法,首先我们就得了解AQS中的同步队列是个什么样的数据结构,因为同步队列是AQS对同步状态管理的基石


同步队列

当共享资源被某个线程占有,其他请求该资源的线程将会阻塞,从而进入同步队列。AQS中的同步队列则是通过链式方式进行实现。在AQS有一个静态内部类Node,这是我们同步队列的每个具体节点。在这个类中有如下属性:

在这里插入图片描述
现在我们知道了节点的数据结构类型,并且每个节点拥有其前驱和后继节点,很显然这是一个双向队列。AQS实际上通过头尾指针来管理同步队列,同时实现包括获取锁失败的线程进行入队,获取锁成功的线程进行出队,释放锁时对同步队列中的线程进行通知等核心方法。其示意图如下:
在这里插入图片描述


AQS的底层实现


独占锁
独占锁的获取

调用lock()方法是获取独占锁,获取失败就将当前线程加入同步队列,成功则线程执行。

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

使用CAS来尝试将同步状态改为1,若成功则将同步状态持有线程置为当前线程。否则将调用AQS提供的aquire()方法

public final void acquire(int arg) {
  // 再次尝试获取同步状态,如果成功则方法直接返回
  // 如果失败则先调用addWaiter()方法再调用acquireQueued()方法
  if (!tryAcquire(arg) &&
    acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    selfInterrupt();
}

使用acquire方法再次获取同步状态,若成功则直接返回,若失败就将当前线程加入同步队列,再排队继续获取锁
上述过程可理解如下图:
在这里插入图片描述
接下来我们就分别来研究一下addWaiter()方法和acquireQueued()方法。

addWaiter()方法——入队
private Node addWaiter(Node mode) {
  // 将当前线程包装称为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;
    if (compareAndSetTail(pred, node)) {
      pred.next = node;
      return node;
   }
 }
  // 当前尾节点为空或CAS尾插失败
  enq(node);
  return node;
}

通过上述的代码我们可以发现,addWaiter()方法的流程图如下:
在这里插入图片描述
通过上述流程图我们可以发现,enq()方法的执行结果一定是成功的,那么原因是什么呢?我们来分析一下enq()的源码:

private Node enq(final Node node) {
  for (;;) {
    Node t = tail;
    if (t == null) { // Must initialize
      // 头结点初始化
      if (compareAndSetHead(new Node()))
        tail = head;
   } else {
      node.prev = t;
      // CAS尾插,失败进行自旋重试直到成功为止。
      if (compareAndSetTail(t, node)) {
        t.next = node;
        return t;
     }
   }
 }
}

通过上述代码我们可以发现,enq()的流程图如下:
在这里插入图片描述
现在我们已经很清楚获取独占式锁失败的线程包装成Node然后插入同步队列的过程了,那么我们就要清楚在同步队列中的结点(线程)如何来保证自己能够有机会获得独占式锁了,来分析一下acquireQueued()方法。

acquireQueued()方法——排队获取锁
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);
 }
}

通过上述代码我们可以发现,acquireQueued()方法的流程图如下:

在这里插入图片描述
经过上面的分析,独占式锁的获取过程如下:

在这里插入图片描述

独占式锁的释放

独占式锁的释放调用unlock()方法,而该方法实际调用了AQS的release方法
unlock()方法:

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

release()方法:

public final boolean release(int arg) {
  if (tryRelease(arg)) {
    Node h = head;
    if (h != null && h.waitStatus != 0)
      unparkSuccessor(h);
    return true;
 }
  return false;
}

通过上述代码我们可以发现,独占式锁的释放过程如下:

在这里插入图片描述
独占式锁的总结:
1. 线程获取锁失败,线程被封装成Node进行入队操作,核心方法在于addWaiter()和enq(),同时enq()完成对同步队列的头结点初始化工作以及CAS操作失败的重试;
2. 线程获取锁是一个自旋的过程,当且仅当 当前节点的前驱节点是头结点并且成功获得同步状态时,节点出队即该节点引用的线程获得锁,否则,当不满足条件时就会调用LookSupport.park()方法使得线程阻塞;
3. 释放锁的时候会唤醒后继节点;

总体来说:在获取同步状态时,AQS维护一个同步队列,获取同步状态失败的线程会加入到队列中进行自旋;移除队列(或停止自旋)的条件是前驱节点是头结点并且成功获得了同步状态。在释放同步状态时,同步器会调用unparkSuccessor()方法唤醒后继节点。


可中断式获取锁

可响应中断式锁可调用方法lock.lockInterruptibly(),而该方法其底层会调用AQS的acquireInterruptibly方法。

public final void acquireInterruptibly(int arg)
    throws InterruptedException {
  if (Thread.interrupted())
    throw new InterruptedException();
  if (!tryAcquire(arg))
    // 线程获取锁失败
    doAcquireInterruptibly(arg);
}

获取同步状态失败后就会调用doAcquireInterruptibly方法

private void doAcquireInterruptibly(int arg)
	throws InterruptedException {
	// 将节点插入到同步队列中
	final Node node = addWaiter(Node.EXCLUSIVE);
	boolean failed = true;
	try {
		for (;;) {
			final Node p = node.predecessor();
			// 获取锁出队
			if (p == head && tryAcquire(arg)) {
				setHead(node);
				p.next = null; // help GC
				failed = false;
				return;
			}
			if (shouldParkAfterFailedAcquire(p, node) &&
			    parkAndCheckInterrupt())
			    // 线程中断异常
			    throw new InterruptedException();
			}
	} finally {
	     if (failed)
		cancelAcquire(node);
	}
}

上述代码与acquire方法逻辑几乎一致,唯一的区别当parkAndCheckInterrupt返回true时(即线程阻塞时)该线程被中断,代码抛出被中断异常


超时等待式获取锁

通过调用lock.tryLock(timeout,TimeUnit)方式达到超时等待获取锁的效果,该方法会在三种情况下才会返回:
1. 在超时时间内,当前线程成功获取了锁;
2. 当前线程在超时时间内被中断;
3. 超时时间结束,仍未获得锁返回false。

该方法会调用AQS的方法tryAcquireNanos()。

public final boolean tryAcquireNanos(int arg, long nanosTimeout)
    throws InterruptedException {
  if (Thread.interrupted())
    throw new InterruptedException();
  return tryAcquire(arg) ||
    // 实现超时等待的效果
    doAcquireNanos(arg, nanosTimeout);
}

最终是靠doAcquireNanos方法实现超时等待的效果:

private boolean doAcquireNanos(int arg, long nanosTimeout)
    throws InterruptedException {
  if (nanosTimeout <= 0L)
    return false;
  // 1.根据超时时间和当前时间计算出截止时间
  final long deadline = System.nanoTime() + nanosTimeout;
  final Node node = addWaiter(Node.EXCLUSIVE);
  boolean failed = true;
  try {
    for (;;) {
      final Node p = node.predecessor();
      // 2.当前线程获得锁出队列
      if (p == head && tryAcquire(arg)) {
        setHead(node);
        p.next = null; // help GC
        failed = false;
        return true;
     }
      // 3.1 重新计算超时时间
      nanosTimeout = deadline - System.nanoTime();
      // 3.2 已经超时返回false
      if (nanosTimeout <= 0L)
        return false;
      // 3.3 线程阻塞等待
      if (shouldParkAfterFailedAcquire(p, node) &&
        nanosTimeout > spinForTimeoutThreshold)
        LockSupport.parkNanos(this, nanosTimeout);
      // 3.4 线程被中断抛出被中断异常
      if (Thread.interrupted())
        throw new InterruptedException();
   }
 } finally {
    if (failed)
      cancelAcquire(node);
 }
}      

通过上述代码我们可以发现,doAcquireNanos方法的程序流程如下:
在这里插入图片描述
程序逻辑同独占锁可响应中断式获取基本一致,唯一的不同在于获取锁失败后,对超时时间的处理上


注:上述内容仅为自己在学习过程中的理解,如有不足之处,请指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值