前面已经或粗或细地看过了 AbstractQueuedSynchronizer 的源码,不妨趁热打铁把其常见的实现的源码也翻一遍,于是我们先来看 ReentrantLock 内部是怎么实现的。

一. 简介

ReentrantLock 顾名思义是一个可重入锁,它是基于 AbstractQueuedSynchronizer 实现的,支持公平锁和非公平锁机制。它实现了 java.util.concurrent.locks.Lock 以及 java.io.Serializable 接口。

1
2
3
public class ReentrantLock implements Lock, java.io.Serializable {
	private final Sync sync;
}

它内部持有了一个 Sync 内部抽象类对象,加锁和解锁的逻辑转发具体的 Sync 子类方法上实现,有点类似 Mybatis 里面SessionExecutor 间用到的门面设计模式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public void lock() {
    sync.lock();
}
public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
}
public boolean tryLock() {
    return sync.nonfairTryAcquire(1);
}
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public void unlock() {
    sync.release(1);
}
// ......

Sync 其实是一个 AbstractQueuedSynchronizer 抽象实现,其中实现了AbstractQueuedSynchronizer 中的部分方法,其子类 FairSyncNonfairSync 也实现了部分方法,剩下的继承 AbstractQueuedSynchronizer 中的实现,下面是相关的类关系图。

Snipaste_2021-08-17_13-43-24.png

顾名思义,NonfairSync 是非公平锁实现,FairSync 是公平锁实现,而公平锁与非公平的区别只在当前线程获取锁是否需要排队这一判断,下面说到加锁的时候会提及。

先看 ReentrantLock 的构造函数,默认会创建一个 NonfairSync 类型的 Sync 实现,也就是说 ReentrantLock 默认提供的是非公平锁。

1
2
3
public ReentrantLock() {
    sync = new NonfairSync();
}

二. 非公平锁

非公平锁获取锁时不需要考虑当前线程是否需要排队问题,任何尝试获取锁的线程都可以直接进行通过 CAS 操作,因为如果此刻有线程在排队,而后来者却可以先于它们获取到锁,即 ”先到不一定先得“,所以称之为非公平锁,这种机制可能导致 ”线程饿死“ 的情况出现,即排队的线程可能非常倒霉一直被人插队,自己一直拿不到锁,对应的任务也迟迟不能执行。

1. 获取锁

NonfairSync 中只重写了获取锁相关的方法,默认地,调用 ReentrantLock 的 lock 方法,将调用 NonfairSync 的 lock 方法,其中不会判断当前队列情况,而是在第一时间 compareAndSetState ,如果加锁成功,则当前线程成为独占线程。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public static final class NonfairSync extends Sync {
    @Override
    final void lock() {
        if (compareAndSetState(0, 1)) {
            setExclusiveOwnerThread(Thread.currentThread());
        } else {
            acquire(1);
        }
    }

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

如果加锁失败,则会调用 AbstractQueuedSynchronizer 中的 acquire 模板方法,其中又会回调 NonfairSync 中的 tryAcquire 方法,而 tryAcquire 则会复用 Sync 中的 nonfairTryAcquire 方法。

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

所以实际的非公平尝试加锁逻辑在 nonfairTryAcquire ,其中主要的判断当前锁是否可获取

  • 如果是则尝试 compareAndSetState,将 state 从 0 改成 1 进行加锁
  • 如果锁已经被其他线程占有,则判断是否是当前线程占有的,是则重入
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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;
}

如果加锁失败,则会执行 AbstractQueuedSynchronizer 的 acquire 方法中 acquireQueued(addWaiter(Node.EXCLUSIVE), arg) 代码,这块的执行过程在前面介绍 AbstractQueuedSynchronizer 时已经讲过,大概流程就是将当前线程节点化入队然后判断是否需要阻塞,是则将前继节点的 waitSttatus 改成 -1,然后自己阻塞。

2. 释放锁

非公平锁释放锁调用的是 AbstractQueuedSynchronizer 中的模板方法 release,其中会回调 Sync 中的钩子函数 tryRelease,如果tryRelease 返回成功,表示释放锁成功,则会判断队列是否初始化,是的话则尝试唤醒队列中的第一个非哨兵非取消节点。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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 方法,逻辑比较简单,如果 state 被减为 0,则将独占线程指针置空返回 true。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

三. 公平锁

公平锁加锁是直接调用 AbstractQueuedSynchronizer 的 acquire 方法,这样能复用入队和阻塞的逻辑,至于自己的定制化逻辑,则由钩子函数 tryAcquire 实现。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
static final class FairSync extends Sync {
    @Override
    final void lock() {
        acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}

与非公平锁不同,公平锁在 tryAcquire 中需要判断此刻是否有线程在排队,如果没有则直接 CAS 获取锁,获取成功则设置当前线程为独占线程,返回 true 结束;如果当前有线程在排队,则由于要实现公平,遵循先到先得原则,则当前线程也需要排队,所以直接返回 false,回到 AbstractQueuedSynchronizer 里面的 acquire 方法中执行 acquireQueued(addWaiter(Node.EXCLUSIVE), arg) 代码即可。

1
2
3
4
5
6
7
public final boolean hasQueuedPredecessors() {
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

hasQueuedPredecessors 的判断极其复杂,需要考虑的场景有很多,在此先记住这个方法的返回值就如方法名所说的,如果返回 true,就表示有线程在等待,当前线程也要排队,否则说明没线程在排队,当前线程可以直接尝试获取锁。

  1. 返回 true,表示当前有线程在排队:

    1. h != t && (s = h.next) == null:队列已经初始化,但是不存在非哨兵节点,可能的情况是:第一个非哨兵节点在 enq·方法入队时初始化哨兵节点,将 head 指向哨兵节点,但是还没有将 tail 指向哨兵,也还没将当前线程所在节点追加到其后。这种情况可以认为有其他线程在排队了,所以返回 true。

      Snipaste_2021-08-17_19-30-22.png

    2. h != t && s.thread != Thread.currentThread():队列已经初始化,并且哨兵的后继节点不是当前节点,因为有其他线程在排队,所以返回 true。

  2. 返回 false,表示当前没有有线程在排队:

    1. h == t:
      1. 队列没有初始化,h 和 t 都为 null,返回 false,如果 CAS 还能省掉初始化队列的操作。
      2. 队列初始化了,但是 h 和 t 都指向一个节点,这种情况只能是都指向哨兵节点,有可能是队列已经初始化了一段时间,排队的节点陆续出队,最后只剩下哨兵节点,由于不存在其他非哨兵节点,所以不需要排队,返回 false。
    2. h != t && ((s = h.next) != null && s.thread == Thread.currentThread()):队列已经初始化,并且哨兵的后继节点不为空,并且后继节点的线程是当前线程,说明当前线程已经在排队,那么就不需要再排队,直接调用 CAS 获取锁。

上面是公平锁加锁的大致过程,至于解锁过程则和非公平锁是一样,都是复用 Sync 中的 release 方法。

四. Q&A

1. hasQueuedPredecessors 当线程发现自己在排队是什么场景?会添加重复节点吗?

hasQueuedPredecessors 中存在一种情况是,当满足 h != t && ((s = h.next) != null && s.thread == Thread.currentThread()) 时,将会返回 false,说明队列中只有当前线程在排队,不需要等待,直接尝试 CAS,那么队列中是不是会有两个当前线程的节点呢?

我在网上看到 这篇文章,这个问题便出自其中,但是我看这个交流,我感到疑惑,首先当第一个非哨兵节点被唤醒时,它是不可能 CAS 失败的,因为这是公平锁,不可能存在其他线程与之竞争,由此我感觉楼主的解答有问题,但是我自己也产生了一个疑问,到底什么场景下一个线程进入 hasQueuedPredecessors 方法会发现自己在排队呢?

Snipaste_2021-08-17_22-37-59.png

带着疑问看到 国外的一篇文章 讲得挺合理,其实该场景并不复杂,由于节点化线程入队后会在 acquireQueued 方法入队,如果入队后发现其前继节点不是哨兵节点,则会阻塞,当某个时刻它被唤醒,此时前继节点便是 head,说明它是第一个非哨兵节点,便会调用 tryAcquire,进入 hasQueuedPredecessors 方法。

此时进入hasQueuedPredecessorsh != t && ((s = h.next) != null && s.thread == Thread.currentThread()) 便会发现当前节点满足该条件,然后 CAS 便会成功。

img

所以我的理解是,s.thread == Thread.currentThread() 这个判断就是针对第一个非哨兵节点的,如果它被唤醒,那么它就不用再排队了,毕竟排了半天才轮到它,怎么可能还需要继续排队呢,所以它就能拿到锁,至于后面的是否入队的问题就更加不存在了。