参考: https://blog.csdn.net/u011202334/article/details/73188404

实现原理

1.lock方法


ReentrantLock实现原理 - 图1

ReentrantLock调用lock方法会调用 ReentrantLock.NonfairSync#lock (这个是非公平锁的lock方法),
会调用AbstractQueuedSynchronizer#compareAndSetState更新一个state状态值为1(cas操作),有两种情况

1.如果更新成功(说明获取到锁了)就给当前线程设置一个独占锁线程直接返回

2.如果更新失败说明没有获取到锁
然后调用AbstractQueuedSynchronizer#acquire (这是AQS里面的实现),调用 ReentrantLock.NonfairSync#tryAcquire调用ReentrantLock.Sync#nonfairTryAcquire. 这个nonfairTryAcquire方法是boolean类型的,true代表获得锁成功,false代表获取锁失败. 如果在这里返回了false的话,AQS会给当前的线程封装成Node节点加入到同步队列的尾节点进行排队

nonfairTryAcquire内部逻辑是先判断state是否为0(0代表当前是无锁状态),如果是0的话又会通过cas尝试给state设置为1(设置为1意思是给当前线程设置为有锁状态)如果设置成功了就返回true,这么做的意义是因为B第一次获取锁的时候.假如说B线程没有获取成功,进入这个nonfairTryAcquire方法了,获取A线程已经释放锁了呢?所以就又尝试一次获取锁.
如果设置失败了,此时state不等于0说明已经是有锁状态了,判断当前线程是不是独占锁线程(就是当前线程是不是持有锁的线程),如果是的话,就给state值加1(目的表示锁的可重入次数,当state为0的时候说明没有锁,state大于0的时候说明有锁),

  1. final boolean nonfairTryAcquire(int acquires) {
  2. final Thread current = Thread.currentThread();
  3. int c = getState();
  4. if (c == 0) {
  5. if (compareAndSetState(0, acquires)) { //如果设置成功
  6. setExclusiveOwnerThread(current); //给当前线程标示为独占锁线程
  7. return true;
  8. }
  9. }
  10. //上个if判断失败了,说明有锁了,然后判断当前线程是否是独占锁线程,
  11. //如果是的话就给state值基于cas加1(重入锁),
  12. else if (current == getExclusiveOwnerThread()) {
  13. int nextc = c + acquires;
  14. if (nextc < 0) // overflow
  15. throw new Error("Maximum lock count exceeded");
  16. setState(nextc);
  17. return true;
  18. }
  19. return false;
  20. }

如果nonfairTryAcquire返回的false,说明没有获取到锁,就会给当前独占锁封装成Node然后走AbstractQueuedSynchronizer#acquireQueued 如果Node是头节点的话再尝试获取锁,如果恰巧别的线程释放了锁呢,抢到锁成功就返回true,如果获取锁失败了会给线程挂起.

2.unlock


unlock调用AbstractQueuedSynchronizer#release 就是给 state变量减1,当state变为0的时候说明这个线程彻底释放锁了,此时唤醒Node队列队列头节点,头结点被唤醒之后开始出来抢占获取锁.


3.公平锁和非公平锁区别


非公平锁是上来就直接抢锁,公平锁是检查是否有线程在等待获取锁,如果有的话就给当前线程封装成Node节点放入到Node队列尾部,等待排队获取锁

非公平锁获取锁细节代码

  1. final boolean nonfairTryAcquire(int acquires) {
  2. final Thread current = Thread.currentThread();
  3. int c = getState();
  4. if (c == 0) {
  5. if (compareAndSetState(0, acquires)) { //如果设置成功
  6. setExclusiveOwnerThread(current); //给当前线程标示为独占锁线程
  7. return true;
  8. }
  9. }
  10. //上个if判断失败了,说明有锁了,然后判断当前线程是否是独占锁线程,
  11. //如果是的话就给state值基于cas加1(重入锁),
  12. else if (current == getExclusiveOwnerThread()) {
  13. int nextc = c + acquires;
  14. if (nextc < 0) // overflow
  15. throw new Error("Maximum lock count exceeded");
  16. setState(nextc);
  17. return true;
  18. }
  19. return false;
  20. }



公平锁获取锁细节

  1. protected final boolean tryAcquire(int acquires) {
  2. final Thread current = Thread.currentThread();
  3. int c = getState();
  4. if (c == 0) {
  5. // 如果有获取失败的锁在排队了,并且当前线程不是持有锁的线程的话,
  6. //就直接给当前线程放到Node队列里面尾部,让它排队
  7. if (!hasQueuedPredecessors() &&
  8. compareAndSetState(0, acquires)) {
  9. setExclusiveOwnerThread(current);
  10. return true;
  11. }
  12. }
  13. else if (current == getExclusiveOwnerThread()) {
  14. int nextc = c + acquires;
  15. if (nextc < 0)
  16. throw new Error("Maximum lock count exceeded");
  17. setState(nextc);
  18. return true;
  19. }
  20. return false;
  21. }
  22. }


//**

lock() 与 unlock() 实现原理

3.1 基础知识

ReentrantLock 是 Lock 的默认实现之一。那么 lock() 和 unlock() 是怎么实现的呢?首先我们要弄清楚几个概念

  • 可重入锁。可重入锁是指同一个线程可以多次获取同一把锁。ReentrantLock 和 synchronized 都是可重入锁。
  • 可中断锁。可中断锁是指线程尝试获取锁的过程中,是否可以响应中断。synchronized 是不可中断锁,而 ReentrantLock 则提供了中断功能。
  • 公平锁与非公平锁。公平锁是指多个线程同时尝试获取同一把锁时,获取锁的顺序按照线程达到的顺序,而非公平锁则允许线程 “插队”。synchronized 是非公平锁,而 ReentrantLock 的默认实现是非公平锁,但是也可以设置为公平锁。
  • CAS 操作 (CompareAndSwap)。CAS 操作简单的说就是比较并交换。CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。” Java 并发包(java.util.concurrent) 中大量使用了 CAS 操作, 涉及到并发的地方都调用了 sun.misc.Unsafe 类方法进行 CAS 操作。

3.2 内部结构

ReentrantLock 提供了两个构造器,分别是

  1. public ReentrantLock() {
  2. sync = new NonfairSync();
  3. }
  4. public ReentrantLock(boolean fair) {
  5. sync = fair ? new FairSync() : new NonfairSync();
  6. }

默认构造器初始化为 NonfairSync 对象,即非公平锁,而带参数的构造器可以指定使用公平锁和非公平锁。由 lock() 和 unlock 的源码可以看到,它们只是分别调用了sync对象的 lock() 和 release(1) 方法。

Sync 是 ReentrantLock 的内部类,它的结构如下

ReentrantLock实现原理 - 图2

可以看到 Sync 扩展了 AbstractQueuedSynchronizer。

3.3 NonfairSync 从源代码出发,分析非公平锁获取锁和释放锁的过程

我们从源代码出发,分析非公平锁获取锁和释放锁的过程。

3.3.1 lock()

lock() 源码如下

  1. final void lock() {
  2. if (compareAndSetState(0, 1))
  3. setExclusiveOwnerThread(Thread.currentThread());
  4. else
  5. acquire(1);
  6. }

首先用一个 CAS 操作,判断 state 是否是 0(表示当前锁未被占用),如果是 0 则把它置为 1,并且设置当前线程为该锁的独占线程,表示获取锁成功。当多个线程同时尝试占用同一个锁时,CAS 操作只能保证一个线程操作成功,剩下的只能乖乖的去排队啦。

“非公平”即体现在这里,如果占用锁的线程刚释放锁,state 置为 0,而排队等待锁的线程还未唤醒时,新来的线程就直接抢占了该锁,那么就 “插队” 了。

若当前有三个线程去竞争锁,假设线程 A 的 CAS 操作成功了,拿到了锁开开心心的返回了,那么线程 B 和 C 则设置 state 失败,走到了 else 里面。我们往下看 acquire。

acquire(arg)

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

代码非常简洁,但是背后的逻辑却非常复杂,可见 Doug Lea 大神的编程功力。

  1. 第一步。尝试去获取锁。如果尝试获取锁成功,方法直接返回。

tryAcquire(arg)

final boolean nonfairTryAcquire(int acquires) {
    //获取当前线程
    final Thread current = Thread.currentThread();
    //获取state变量值
    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");
        // 更新state值为新的重入次数
        setState(nextc);
        return true;
    }
    //获取锁失败
    return false;
}

非公平锁 tryAcquire 的流程是:检查 state 字段,若为 0,表示锁未被占用,那么尝试占用,若不为 0,检查当前锁是否被自己占用,若被自己占用,则更新 state 字段,表示重入锁的次数。如果以上两点都没有成功,则获取锁失败,返回 false。

  1. 第二步,入队。由于上文中提到线程 A 已经占用了锁,所以 B 和 C 执行 tryAcquire 失败,并且入等待队列。如果线程 A 拿着锁死死不放,那么 B 和 C 就会被挂起。

先看下入队的过程。

先看 addWaiter(Node.EXCLUSIVE)

/**
 * 将新节点和当前线程关联并且入队列
 * @param mode 独占/共享
 * @return 新节点
 */
private Node addWaiter(Node mode) {
    //初始化节点,设置关联线程和模式(独占 or 共享)
    Node node = new Node(Thread.currentThread(), mode);
    // 获取尾节点引用
    Node pred = tail;
    // 尾节点不为空,说明队列已经初始化过
    if (pred != null) {
        node.prev = pred;
        // 设置新节点为尾节点
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 尾节点为空,说明队列还未初始化,需要初始化head节点并入队新节点
    enq(node);
    return node;
}

B、C 线程同时尝试入队列,由于队列尚未初始化,tail==null,故至少会有一个线程会走到 enq(node)。我们假设同时走到了 enq(node) 里。

/**
 * 初始化队列并且入队新节点
 */
private Node enq(final Node node) {
    //开始自旋
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            // 如果tail为空,则新建一个head节点,并且tail指向head
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            // tail不为空,将新节点入队
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

这里体现了经典的自旋 + CAS 组合来实现非阻塞的原子操作。由于 compareAndSetHead 的实现使用了 unsafe 类提供的 CAS 操作,所以只有一个线程会创建 head 节点成功。假设线程 B 成功,之后 B、C 开始第二轮循环,此时 tail 已经不为空,两个线程都走到 else 里面。假设 B 线程 compareAndSetTail 成功,那么 B 就可以返回了,C 由于入队失败还需要第三轮循环。最终所有线程都可以成功入队。

当 B、C 入等待队列后,此时 AQS 队列如下:

ReentrantLock实现原理 - 图3

  1. 第三步,挂起。B 和 C 相继执行 acquireQueued(final Node node, int arg)。这个方法让已经入队的线程尝试获取锁,若失败则会被挂起。
/**
 * 已经入队的线程尝试获取锁
 */
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true; //标记是否成功获取锁
    try {
        boolean interrupted = false; //标记线程是否被中断过
        for (;;) {
            final Node p = node.predecessor(); //获取前驱节点
            //如果前驱是head,即该结点已成老二,那么便有资格去尝试获取锁
            if (p == head && tryAcquire(arg)) {
                setHead(node); // 获取成功,将当前节点设置为head节点
                p.next = null; // 原head节点出队,在某个时间点被GC回收
                failed = false; //获取成功
                return interrupted; //返回是否被中断过
            }
            // 判断获取失败后是否可以挂起,若可以则挂起
            if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                // 线程若被中断,设置interrupted为true
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

code 里的注释已经很清晰的说明了 acquireQueued 的执行流程。假设 B 和 C 在竞争锁的过程中 A 一直持有锁,那么它们的 tryAcquire 操作都会失败,因此会走到第 2 个 if 语句中。我们再看下 shouldParkAfterFailedAcquire 和 parkAndCheckInterrupt 都做了哪些事吧。

/**
 * 判断当前线程获取锁失败之后是否需要挂起.
 */
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    //前驱节点的状态
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        // 前驱节点状态为signal,返回true
        return true;
    // 前驱节点状态为CANCELLED
    if (ws > 0) {
        // 从队尾向前寻找第一个状态不为CANCELLED的节点
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 将前驱节点的状态设置为SIGNAL
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

/**
 * 挂起当前线程,返回线程中断状态并重置
 */
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

线程入队后能够挂起的前提是,它的前驱节点的状态为 SIGNAL,它的含义是 “Hi,前面的兄弟,如果你获取锁并且出队后,记得把我唤醒!”。所以 shouldParkAfterFailedAcquire 会先判断当前节点的前驱是否状态符合要求,若符合则返回 true,然后调用 parkAndCheckInterrupt,将自己挂起。如果不符合,再看前驱节点是否 > 0(CANCELLED),若是那么向前遍历直到找到第一个符合要求的前驱,若不是则将前驱节点的状态设置为 SIGNAL。

整个流程中,如果前驱结点的状态不是 SIGNAL,那么自己就不能安心挂起,需要去找个安心的挂起点,同时可以再尝试下看有没有机会去尝试竞争锁。

最终队列可能会如下图所示

ReentrantLock实现原理 - 图4

线程 B 和 C 都已经入队,并且都被挂起。当线程 A 释放锁的时候,就会去唤醒线程 B 去获取锁啦。

3.3.2 unlock()

unlock 相对于 lock 就简单很多。源码如下

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

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

如果理解了加锁的过程,那么解锁看起来就容易多了。流程大致为先尝试释放锁,若释放成功,那么查看头结点的状态是否为 SIGNAL,如果是则唤醒头结点的下个节点关联的线程,如果释放失败那么返回 false 表示解锁失败。这里我们也发现了,每次都只唤起头结点的下一个节点关联的线程。

最后我们再看下 tryRelease 的执行过程

/**
 * 释放当前线程占用的锁
 * @param releases
 * @return 是否释放成功
 */
protected final boolean tryRelease(int releases) {
    // 计算释放后state值
    int c = getState() - releases;
    // 如果不是当前线程占用锁,那么抛出异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        // 锁被重入次数为0,表示释放成功
        free = true;
        // 清空独占线程
        setExclusiveOwnerThread(null);
    }
    // 更新state值
    setState(c);
    return free;
}

这里入参为 1。tryRelease 的过程为:当前释放锁的线程若不持有锁,则抛出异常。若持有锁,计算释放后的 state 值是否为 0,若为 0 表示锁已经被成功释放,并且则清空独占线程,最后更新 state 值,返回 free。

3.3.3 小结

用一张流程图总结一下非公平锁的获取锁的过程。

ReentrantLock实现原理 - 图5

3.4 FairSync

公平锁和非公平锁不同之处在于,公平锁在获取锁的时候,不会先去检查 state 状态,而是直接执行 aqcuire(1),这里不再赘述。

用一张流程图总结非公平锁的获取锁的过程

非公平锁获取锁流程:

1: 首先不管三七二一就来个 CAS 尝试获取锁。
2: 成功则皆大欢喜。
3: 失败,再次获取下共享状态(万一这会有人释放了尼)判断是否为0
4: 如果为0 则说明锁空闲,再次CAS获取锁成功将持有锁线程设置为自己并返回ture
5:不为0,判断持有者是否是自己、是自己表明可重入state + 1 返回ture 否则返回false(就去同步队列中排队去)。

非公平锁释放锁流程
很简单state – 1 = 0 则释放成功否则失败。

image.png

4 超时机制

在 ReetrantLock 的 tryLock(long timeout, TimeUnit unit) 提供了超时获取锁的功能。它的语义是在指定的时间内如果获取到锁就返回 true,获取不到则返回 false。这种机制避免了线程无限期的等待锁释放。那么超时的功能是怎么实现的呢?我们还是用非公平锁为例来一探究竟。

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);
}

这里的语义是:如果线程被中断了,那么直接抛出 InterruptedException。如果未中断,先尝试获取锁,获取成功就直接返回,获取失败则进入 doAcquireNanos。tryAcquire 我们已经看过,这里重点看一下 doAcquireNanos 做了什么。

/**
 * 在有限的时间内去竞争锁
 * @return 是否获取成功
 */
private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    // 起始时间
    long lastTime = System.nanoTime();
    // 线程入队
    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;
            }
            // 如果已经超时,返回false
            if (nanosTimeout <= 0)
                return false;
            // 超时时间未到,且需要挂起
            if (shouldParkAfterFailedAcquire(p, node) &&
                    nanosTimeout > spinForTimeoutThreshold)
                // 阻塞当前线程直到超时时间到期
                LockSupport.parkNanos(this, nanosTimeout);
            long now = System.nanoTime();
            // 更新nanosTimeout
            nanosTimeout -= now - lastTime;
            lastTime = now;
            if (Thread.interrupted())
                //相应中断
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

doAcquireNanos 的流程简述为:线程先入等待队列,然后开始自旋,尝试获取锁,获取成功就返回,失败则在队列里找一个安全点把自己挂起直到超时时间过期。这里为什么还需要循环呢?因为当前线程节点的前驱状态可能不是 SIGNAL,那么在当前这一轮循环中线程不会被挂起,然后更新超时时间,开始新一轮的尝试

//**

ReentrantLock::lock公平锁模式实现

image.png

首先需要在构建函数中传入 true 创建好公平锁

ReentrantLock reentrantLock = new ReentrantLock(true);

调用 lock() 进行上锁,直接 acquire(1) 上锁

public void lock() {
  // 调用的sync的子类FairSync的lock()方法:ReentrantLock.FairSync.lock()
  sync.lock();
}
final void lock() {
  // 调用AQS的acquire()方法获取锁,传的值为1
  acquire(1);
}

直接尝试获取锁,

// AbstractQueuedSynchronizer.acquire()
public final void acquire(int arg) {
  // 尝试获取锁
  // 如果失败了,就排队
  if (!tryAcquire(arg) &&
    // 注意addWaiter()这里传入的节点模式为独占模式
    acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    selfInterrupt();
}

具体获取锁流程

  • getState() 获取同步状态 state 值,进行判断是否为0:
    • 如果状态变量的值为0,说明暂时还没有人占有锁, 使用hasQueuedPredecessors()保证了不论是新的线程还是已经排队的线程都顺序使用锁,如果没有其它线程在排队,那么当前线程尝试更新state的值为1,并自己设置到exclusiveOwnerThread变量中,供后续自己可重入获取锁作准备。
    • 如果exclusiveOwnerThread中为当前线程说明本身就占有着锁,现在又尝试获取锁,需要将状态变量的值 state+1

image.png

// ReentrantLock.FairSync.tryAcquire()
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    // 状态变量的值为0,说明暂时还没有线程占有锁
    if (c == 0) {
        // hasQueuedPredecessors()保证了不论是新的线程还是已经排队的线程都顺序使用锁
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            // 当前线程获取了锁,并将本线程设置到exclusiveOwnerThread变量中,
            //供后续自己可重入获取锁作准备
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 之所以说是重入锁,就是因为在获取锁失败的情况下,还会再次判断是否当前线程已经持有锁了
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        // 设置到state中
        // 因为当前线程占有着锁,其它线程只会CAS把state从0更新成1,是不会成功的
        // 所以不存在竞争,自然不需要使用CAS来更新
        setState(nextc);
        return true;
    }
    return false;
}

如果获取失败加入队列里,那具体怎么处理呢?通过自旋的方式,队列中线程不断进行尝试获取锁操
作,中间是可以通过中断的方式打断,

  • 如果当前节点的前一个节点为head节点,则说明轮到自己获取锁了,调用 tryAcquire() 方法再次尝试获取锁

    final boolean acquireQueued(final Node node, int arg) {
      boolean failed = true;
      try {
          boolean interrupted = false;
          // 自旋
          for (;;) {
              // 当前节点的前一个节点,
              final Node p = node.predecessor();
              // 如果当前节点的前一个节点为head节点,则说明轮到自己获取锁了
              // 调用ReentrantLock.FairSync.tryAcquire()方法再次尝试获取锁
              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);
      }
    }
    
  • 当前的Node的上一个节点不是Head,是需要判断是否需要阻塞,以及寻找安全点挂起。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 上一个节点的等待状态
    int ws = pred.waitStatus;
    // 等待状态为SIGNAL(等待唤醒),直接返回true
    if (ws == Node.SIGNAL)
        return true;
    // 前一个节点的状态大于0,已取消状态
    if (ws > 0) {
        // 把前面所有取消状态的节点都从链表中删除
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 前一个Node的状态小于等于0,则把其状态设置为等待唤醒
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

在看完获取锁的流程,那么你知道ReentrantLock如何实现公平锁了吗?其实就是在 tryAcquire() 的实现中。

ReentrantLock如何实现公平锁?

在 tryAcquire() 的实现中使用了 hasQueuedPredecessors() 保证了线程先进先出FIFO的使用锁,不会产生”饥饿”问题,

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    // 状态变量的值为0,说明暂时还没有线程占有锁
    if (c == 0) {
        // hasQueuedPredecessors()保证了不论是新的线程还是已经排队的线程都顺序使用锁
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            ....
            }
        ...
        } 
}
public final boolean hasQueuedPredecessors() { 
    Node t = tail;
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

tryAcquire都会检查CLH队列中是否仍有前驱的元素,如果仍然有那么继续等待,通过这种方式来保证先来先服务的原则。
那这样ReentrantLock如何实现可重入?是怎么重入的?

ReentrantLock如何实现可重入?

其实也很简单,在获取锁后,设置一个标识变量为当前线程 exclusiveOwnerThread ,当线程再次进入判断 exclusiveOwnerThread 变量是否等于本线程来判断.

protected final boolean tryAcquire(int acquires) {
    // 状态变量的值为0,说明暂时还没有线程占有锁
    if (c == 0) {
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            // 当前线程获取了锁,并将本线程设置到exclusiveOwnerThread变量中,
            //供后续自己可重入获取锁作准备
            setExclusiveOwnerThread(current);
            return true;
        }
    } //之所以说是重入锁,就是因为在获取锁失败的情况下,还会再次判断是否当前线程已经持有锁了
    else if (current == getExclusiveOwnerThread()) {
        ...
        }
}

当看完公平锁获取锁的流程,那其实我们也了解非公平锁获取锁,那我们来看看。

ReentrantLock公平锁模式与非公平锁获取锁的区别?

其实非公平锁获取锁获取区别主要在于:

  • 构建函数中传入 false 或者为null,为创建非公平锁 NonfairSync , true 创建公平锁,
  • 非公平锁在获取锁的时候,先去检查 state 状态,再直接执行 aqcuire(1) ,这样可以提高效率,

    final void lock() {
        if (compareAndSetState(0, 1))
          //修改同步状态的值成功的话,设置当前线程为独占的线程
          setExclusiveOwnerThread(Thread.currentThread());
        else
          //获取锁
          acquire(1);
     }
    
  • 在 tryAcquire() 中没有 hasQueuedPredecessors() 保证了不论是新的线程还是已经排队的线程都顺序使用锁。

ReentrantLock::unlock()释放锁,如何唤醒等待队列中的线程?

  • 释放当前线程占用的锁

    protected final boolean tryRelease(int releases) {
      // 计算释放后state值
      int c = getState() - releases;
      // 如果不是当前线程占用锁,那么抛出异常
      if (Thread.currentThread() != getExclusiveOwnerThread())
          throw new IllegalMonitorStateException();
      boolean free = false;
      if (c == 0) {
          // 锁被重入次数为0,表示释放成功
          free = true;
          // 清空独占线程
          setExclusiveOwnerThread(null);
      }
      // 更新state值
      setState(c);
      return free;
    }
    
  • 若释放成功,就需要唤醒等待队列中的线程,先查看头结点的状态是否为SIGNAL,如果是则唤醒头结点的下个节点关联的线程,如果释放失败那么返回false表示解锁失败。

    • 设置waitStatus为0,
    • 当头结点下一个节点不为空的时候,会直接唤醒该节点,如果该节点为空,则会队尾开始向前遍历,找到历,找到最后一个不为空的节点,然后唤醒。
private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    Node s = node.next;//这里的s是头节点(现在是头节点持有锁)的下一个节点,也就是期望唤醒的节点
        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); //唤醒s代表的线程
}