AQS源码分析(三):公平锁、非公平锁、等待超时

184 阅读6分钟

概述

这篇文章算是前两篇文章的补充,这里比较集中的讲解非公平锁、公平锁以及各种锁的超时等待的逻辑。

1.公平锁与非公平锁

1.1.ReentrantLock

非公平模式,其实就是无论等待队列中是否还有其他排队的节点,都在 lock 的时候立刻尝试获取一次锁,这里我们拿ReentrantLock举个简单的例子:

// 非公平锁的 lock 方法
final void lock() {
  if (compareAndSetState(0, 1))
      setExclusiveOwnerThread(Thread.currentThread());
  else
      acquire(1);
}

// 公平锁的 lock 方法
final void lock() {
  acquire(1);
}

一目了然。

1.2.ReentrantReadWriteLock

ReentrantReadWriteLock 中,由于区分读写锁,因此它稍微绕了一层。在 Sync 内部类中,提供了两个抽象方法:

/**
 * Returns true if the current thread, when trying to acquire
 * the read lock, and otherwise eligible to do so, should block
 * because of policy for overtaking other waiting threads.
 */
abstract boolean readerShouldBlock();

/**
 * Returns true if the current thread, when trying to acquire
 * the write lock, and otherwise eligible to do so, should block
 * because of policy for overtaking other waiting threads.
 */
abstract boolean writerShouldBlock();

注释也说的很明白了,那就是获取锁的时候,判断一下到底能不能真正的获取锁。 这是公平锁的实现:

// 不论读写锁,只要队列里有排队的线程,全部都乖乖去排队
final boolean writerShouldBlock() {
    return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
    return hasQueuedPredecessors();
}

这是非公平锁的实现:

// 不管队列里有没有排队的线程都试一下
final boolean writerShouldBlock() {
	return false;// writers can always barge
}

// 如果排队的线程抢的是写锁,那就不插队,否则就插队试试
final boolean readerShouldBlock() {
	/* As a heuristic to avoid indefinite writer starvation,
	 * block if the thread that momentarily appears to be head
	 * of queue, if one exists, is a waiting writer.  This is
	 * only a probabilistic effect since a new reader will not
	 * block if there is a waiting writer behind other enabled
	 * readers that have not yet drained from the queue.
	 */
	return apparentlyFirstQueuedIsExclusive();
}

final boolean apparentlyFirstQueuedIsExclusive() {
  Node h, s;
  return (h = head) != null && // 有头结点,说明有线程正在持有锁
      (s = h.next)  != null && // 头结点有后继节点,说明有节点在等待锁
      !s.isShared()         && // 是否共享模式,即这个等待锁的线程要的是写锁
      s.thread != null;
}

// Node 内部类中的方法,此处可见 addWaiter 时指定的模式派上了用场
final boolean isShared() {
  return nextWaiter == SHARED;
}

非公平锁也很简单,写锁无论如何都会去抢一下,而读锁需要确认在排队的线程是不是要抢写锁,如果是那就不插队了,否则直接插队。

2.ReentrantLock的超时机制

我们会注意到 Lock 接口提供了两种 tryLock 方法:

  • boolean tryLock(long time, TimeUnit unit); :等待指定时间,在指定时间内拿不到锁就失败;
  • boolean tryLock(); :只尝试一次,没拿到锁立刻失败;

在阅读过前两章后,我们很容易猜到它们各种的实现逻辑。

2.1.不指定超时时间

ReentrantLock 中,tryLock 最终会调用到 Sync 内部类的 nonfairTryAcquire 方法:

public boolean tryLock() {
  return sync.nonfairTryAcquire(1);
}

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) // overflow
          throw new Error("Maximum lock count exceeded");
      setState(nextc);
      return true;
  }
  return false;
}

等同于简单粗暴的调用了非公平锁的 tryAcquire 方法:

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

2.2.指定超时时间

当指定超时时间时,会先统一的把时间转为纳秒,然后调用 AQS 的 tryAcquireNanos 方法:

public boolean tryLock(long timeout, TimeUnit unit)
      throws InterruptedException {
  return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

public final boolean tryAcquireNanos(int arg, long nanosTimeout)
      throws InterruptedException {
  if (Thread.interrupted())
      throw new InterruptedException();
  return tryAcquire(arg) ||
      doAcquireNanos(arg, nanosTimeout);
}

一如既往地先用 tryAcquire 抢一下锁,失败才真正的进入 doAcquireNanos 方法中:

private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (nanosTimeout <= 0L)
        return false;
    final long deadline = System.nanoTime() + nanosTimeout;
    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 true;
            }
            nanosTimeout = deadline - System.nanoTime();
            if (nanosTimeout <= 0L)
                return false;
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > spinForTimeoutThreshold) // spinForTimeoutThreshold 是个内部静态常量,它表示 AQS 中默认的自旋超时时间,默认为 1000L
                LockSupport.parkNanos(this, nanosTimeout);
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

doAcquireNanos 方法与正常加锁的时在队列中等锁的 acquireQueued 方法逻辑基本一致,但是多了时间检测:

  1. 根据当前时间,以及指定的等待时间,计算超时的时间点 deadline
  2. 进入循环,尝试 CAS 获取锁, 当获取锁失败时:
    1. 计算当前时间与 deadline 之间的时间差 nanosTimeout,如果已经小于 0 ,即当前已经超时了,那么直接中断循环加锁失败;
    2. 如果时间差 nanosTimeout 大于 1000 纳秒,那么就调用 LockSupport.parkNanos 挂起线程。 直到被前驱节点唤醒,或者到了 deadline 后再起来抢锁;
    3. 如果时间差 nanosTimeout 小于 1000 纳秒,说明距离 deadline 没剩多少时间了,直接继续抢锁,没必要再挂起了;
    4. 如果线程中断了,就直接抛异常;
  3. 如果在上述循环中抛出异常(比如调用 tryAcquire 时),那就调用 cancelAcquire 移除当前节点并唤醒后继节点,否则就正常的退出;

我们需要关注在这个方法中,线程竞争锁的几个特殊行为:

  • 首先,进来立刻抢一次锁,抢不到就进等待队列;
  • 在等待队列里等锁,总是默认等到 deadline,不过在这个过程中有机会被前驱节点在deadline 前唤醒,因此有机会多抢几次;
  • 如果醒了以后离deadline不到 1000 纳秒了,那直接不需要挂起,反复疯狂 cas 直到超时;

3.ReentrantReadWriteLock的超时机制

读写锁在不指定超时时间时逻辑基本差不多,因此我们重点关注读写锁在指定超时时间时的实现。

3.1.读锁

public boolean tryLock(long timeout, TimeUnit unit)
      throws InterruptedException {
  return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
      throws InterruptedException {
  if (Thread.interrupted())
      throw new InterruptedException();
  return tryAcquireShared(arg) >= 0 ||
      doAcquireSharedNanos(arg, nanosTimeout);
}

独占锁调用的是 doAcquireNanos ,而非独占锁调用的是 doAcquireSharedNanos

	private boolean doAcquireSharedNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (nanosTimeout <= 0L)
        return false;
    final long deadline = System.nanoTime() + nanosTimeout;
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null;// help GC
failed = false;
                    return true;
                }
            }
            nanosTimeout = deadline - System.nanoTime();
            if (nanosTimeout <= 0L)
                return false;
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout >spinForTimeoutThreshold)
                LockSupport.parkNanos(this, nanosTimeout);
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

doAcquireNanos  的代码不能说一模一样,只能说完全一致,唯一改变的地方只有:

  • 加锁调用的是 tryAcquireShared 而不是 tryAcquire
  • 成功加锁后,调用的是 setHeadAndPropagate 而不是 setHead

3.2.写锁

写锁这边与 ReentrantLock  完全一致,都是通过 AQS 的 tryAcquireNanos 完成:

public boolean tryLock(long timeout, TimeUnit unit)
      throws InterruptedException {
  return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

public final boolean tryAcquireNanos(int arg, long nanosTimeout)
      throws InterruptedException {
  if (Thread.interrupted())
      throw new InterruptedException();
  return tryAcquire(arg) ||
      doAcquireNanos(arg, nanosTimeout);
}

总结

公平锁和非公平锁

  • 对于 ReentrantLock,公平锁和非公平锁的主要区别在于,在调用 Lock 之前会多做一次判断。如果是公平锁,在有排队线程时会直接去排队,而非公平锁无论如何都会试着抢一次锁;
  • 对于 ReentrantReadWriteLock,它的公平锁的逻辑与 ReentrantLock 一致,即无论读写锁,在当前队列有排队线程的情况下都会直接去排队。但是对于非公平锁中的读锁,只有等待队列的前驱节点为共享节点时才会尝试抢占锁;

等待超时

不管是 ReentrantLock 还是 ReentrantReadWriteLock,当调用 tryLock 并且指定超时时间时,都会将超时时间转为纳秒并且调用 AQS 的 tryAcquireNano/tryAcquireSharedNano 方法,在这些方法中,线程将会:

  1. 先根据指定的操作时间计算出真正的超时时间点 deadline
  2. 循环的尝试通过 cas 加锁:
    1. 如果加锁成功,那么直接返回;
    2. 如果加锁失败,且距离 deadline 大于 1000 纳秒,就调用 LockSupport.parkNano 让当前线程挂起到deadline,在这个过程中,如果被前驱节点唤醒,那就继续重试这个步骤,否则就真等到 deadline 后才会被唤醒;
    3. 如果加锁失败,且距离 deadline 小于 1000 纳秒,那么当前线程无需挂起,直接循环尝试获取锁,直到获取锁或者超时为止;