ReentrantrantLock 底层实现原理

一、类图

        ReentrantrantLock 的类图如下所示,可以看到,它也是实现了 Lock 接口,和我们自定义不可重入锁的步骤是一样的,其内部也是维护了一个内部类同步器类 Sync,这个同步器类也是继承自 AQS,不过需要注意的是这里面的同步器类 Sync 是抽象的,有两个实现,一个是非公平锁实现 NonfairSync,一个是公平锁实现 FairSync

二、非公平锁实现原理

2.1 加锁流程

        先从构造器开始看,默认为非公平锁实现,如下代码,其中 NonfairSync 继承自 AQS

    public ReentrantLock() {
        sync = new NonfairSync();
    }

        先看加锁流程,如下的 lock() 方法,可以看到,方法内部调用的是 NonfairSync 实现的 lock() 方法

    public void lock() {
        sync.lock();
    }

        接下来我们看下 NonfairSync lock() 方法是如何实现的,如下图:

        当没有竞争的时候,通过 CAS state 0 改成 1,并把 owner 线程改成自己,如下图:

        当第一个竞争出现时,如下图,此时 owner 已经是 Thread0 了,此时来了一个 Thread-1,此时 Thread-1 还是执行 lock() 方法。

        此时 Thread-1 的 CAS 肯定是失败的,此时就进入 acquire() 方法,此方法的内容如下所示:

        1、进入 tryAcquire 逻辑,这时 state 已经是 1,结果仍然失败。 

        2、接下来进入 addWaiter 逻辑,构造 Node 队列,如下图,会在 head 后面添加一个节点,这个节点是双向链表,即 head 指向一个 node 节点,node 节点又指向下一个 node 节点。


        

        图中黄色三角表示该 Node waitStatus 状态,其中 0 为默认正常状态,Node 的创建是懒惰的。

        需要注意的是,首次创建 node 时会创建两个,其中第一个 Node 称为 Dummy(哑元)或哨兵,用来占位,并不关联线程。只有第二个 Node 才会关联线程 Thread-1,因为它竞争失败了,因为它是第一个加入的,所以它指向了链表的尾部。其实这个就是我们的等待队列。

        等到创建成功之后,当前线程就会进入到 acquireQueued 逻辑,如下图,这个方法主要是在一个死循环中不断尝试获得锁,失败后进入 park 阻塞。

         1、首先获取前驱节点,然后检查前驱节点,判断前驱阶段是否为头节点,如果是,说明 Thread-1 的这个 Node 是处于第二位的,那么它就有资格再次调用 tryAcquire() 方法再次获取锁。

        2、如果还是没有获取到锁,此时就会进入到 shouldParkAfterFailedAcquire 逻辑,这个方法会把当前线程节点的前驱节点的 waitStatus 改为 -1,改为 -1 的意思是它有责任唤醒它的后继节点,此时方法返回 false,如下图:


        3、shouldParkAfterFailedAcquire 执行完毕回到 acquireQueued ,再次 tryAcquire 尝试获取锁,当然这时state 仍为 1,失败。

        4、当再次进入 shouldParkAfterFailedAcquire 时,这时因为其前驱 node waitStatus 已经是 -1,这次返回 true

        5、此时就会进入 parkAndCheckInterrupt(),即 Thread-1 被阻塞住,用灰颜色表示,如下图:

        再次有多个线程经历上述过程竞争失败,就会变成如下的样子:

 2.2 解锁流程

        解锁流程,如下的 lock() 方法,可以看到,方法内部调用的同步器 AQS release() 方法。

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

        假设此时 Thread-0 释放锁,就会进入到 tryRelease() 方法,如果返回 true,就证明释放成功了,就会把 owner 线程设置为 null,并且把 state 设置为 0,如下图:

        tryRelease() 方法返回 true 之后,还需要检查队列中的 head 是否为空,还需要检查 head 中的 waitState 是否不等于 0 ,若返回 true ,则需要调用 unparkSuccessor() 方法唤醒下一个节点。

        unparkSuccessor() 方法是找到队列中离 head 最近的一个 Node(没取消的),unpark 恢复其运行,本例中即为 Thread-1,此时回到 Thread-1 的 acquireQueued() 方法的流程,如下图:

        如果加锁成功(没有竞争),会设置 exclusiveOwnerThread Thread-1state = 1head 指向刚刚 Thread-1 所在的 Node,该 Node 清空 Thread;原本的 head 因为从链表断开,而可被垃圾回收。

        如果这时候有其它线程来竞争(非公平的体现),例如这时有 Thread-4 来了,Thread-4 并不是阻塞队列里面的线程,如下图:

        如果不巧又被 Thread-4 占了先,那么 Thread-4 被设置为 exclusiveOwnerThreadstate = 1;Thread-1 再次进入 acquireQueued 流程,获取锁失败,重新进入 park 阻塞。

三、可重入原理

        接下来我们看下非公平锁的可重入的实现原理,我们看下他的获取锁和释放锁的相关代码,

        获取锁最终调用的是 nonfairTryAcquire() 方法,传入的 acquires 就是 1,首先判断 state 的状态是否是 0,如果是 0 就代表还没有别人获取锁,然后我就去 CAS ,如果成功则设置 owner 设置为自己,然后返回 true

        如果是同一个线程下次又调用 nonfairTryAcquire() 方法,此时就回去判断当前线程是否为 owner 持有的线程,如果是,则证明发生了锁重入,此时就是将 state 的值 ++,由 1 变成了 2

static final class NonfairSync extends Sync {
    // ...

    // Sync 继承过来的方法, 方便阅读, 放在此处
    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()) {
            // state++
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}

        锁释放的时候调用的是 tryRelease() 方法,如下所示,此时就会把 state 的值 --,此时返回一个 false,表示我只是让 state -- ,并没有真正的释放锁,直到下面的这个 c 变成 0 了,才会释放锁,返回 true


    // Sync 继承过来的方法, 方便阅读, 放在此处
    protected final boolean tryRelease(int releases) {
        // state--
        int c = getState() - releases;
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        // 支持锁重入, 只有 state 减为 0, 才释放成功
        if (c == 0) {
            free = true;
            setExclusiveOwnerThread(null);
        }
        setState(c);
        return free;
    }

四、可打断原理

4.1 不可打断模式

        在次模式下,即使它被打断,仍会驻留在 AQS 队列中,一直要等到获得锁后方能得知自己被打断了。

        我们都知道线程在知道自己没有办法获取锁的时候就会进入到 acquireQueued() 方法的循环内不断的去尝试,如果还不成功,就会进入到 parkAndCheckInterrupt() 方法里面等待。

        但是进入到 parkAndCheckInterrupt() 方法的线程可以被其他的线程调用它的 interrupted() 方法唤醒,如下面的 parkAndCheckInterrupt() 方法,会返回一个 boolean 表示是否被打断过,但是 interrupted() 方法还会清除打断标记,即下次再 park 还可以 park 住。

        如果 parkAndCheckInterrupt() 返回 true 了就会进入 if 块了,此时就会对打断标记做一个记录,设置值为 true,没有做额外的处理,接下来还是会继续 for 循环,获取不了锁还是会 park 阻塞。只有当你获取到锁以后,会把打断标记作为结果返回。

        此时就会进入到 selfInterrupt() 方法,重新产生一次中断。

static final class NonfairSync extends Sync {
    // ...

    private final boolean parkAndCheckInterrupt() {
        // 如果打断标记已经是 true, 则 park 会失效
        LockSupport.park(this);
        // interrupted 会清除打断标记
        return Thread.interrupted();
    }

    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;
                    failed = false;
                    // 还是需要获得锁后, 才能返回打断状态
                    return interrupted;
                }
                if (
                        shouldParkAfterFailedAcquire(p, node) &&
                                parkAndCheckInterrupt()
                ) {
                    // 如果是因为 interrupt 被唤醒, 返回打断状态为 true
                    interrupted = true;
                }
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

    public final void acquire(int arg) {
        if (
                !tryAcquire(arg) &&
                        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
        ) {
            // 如果打断状态为 true
            selfInterrupt();
        }
    }

    static void selfInterrupt() {
        // 重新产生一次中断
        Thread.currentThread().interrupt();
    }
}

4.2 可打断模式

        可打断模式就是在调用 acquireInterruptibly() 方法获取锁时可以被打断,它和上面的方法区别是当线程被唤醒的时候,即调用 doAcquireInterruptibly() 时会抛出一个异常,打断 for 循环等待,即线程停止去等待锁

// Sync 继承自 AQS
static final class NonfairSync extends Sync {
    public final void acquireInterruptibly(int arg) throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        // 如果没有获得到锁, 进入 ㈠
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }

    // ㈠ 可打断的获取锁流程
    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()) {
                    // 在 park 过程中如果被 interrupt 会进入此
                    // 这时候抛出异常, 而不会再次进入 for (;;)
                    throw new InterruptedException();
                }
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
}

五、公平锁实现原理

        公平锁和非公平锁的区别主要就是在 tryAcquire() 方法的实现中,当 c==0 即还没有别人占这个锁时,先调用 hasQueuedPredecessors() 方法判断队列中是否有前驱节点,没有才回去竞争,即只能是老二节点线程才可以运行,非老二节点线程不能运行。

static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

    final void lock() {
        acquire(1);
    }

    // AQS 继承过来的方法, 方便阅读, 放在此处
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
                        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
        ) {
            selfInterrupt();
        }
    }

    // 与非公平锁主要区别在于 tryAcquire 方法的实现
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            // 先检查 AQS 队列中是否有前驱节点, 没有才去竞争
            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;
    }

    // ㈠ AQS 继承过来的方法, 方便阅读, 放在此处
    public final boolean hasQueuedPredecessors() {
        Node t = tail;
        Node h = head;
        Node s;
        // h != t 时表示队列中有 Node
        return h != t &&
                (
                        // (s = h.next) == null 表示队列中还有没有老二
                        (s = h.next) == null ||
                                // 或者队列中老二线程不是此线程
                                s.thread != Thread.currentThread()
                );
    }
}

六、条件变量实现原理

        每个条件变量其实就对应着一个等待队列,其实现类就是 ConditionObject

6.1 await 流程

        假设一开始 Thread-0 持有锁,如下图所示:

        接下来如果线程调用了 ConditionObject 类的 await() 方法,它就会进入到 ConditionObject addConditionWaiter 流程,如下图:

        addConditionWaiter () 方法会把调用的线程加入到条件变量里面的双向链表之中,并且把新的 Node 状态变为 -2,表示在条件变量中处于等待状态,如下图:

        接下来会进入到 AQS fullyRelease 流程,释放同步器上的锁,如下图:

        接下来会 unpark AQS 队列中的下一个节点,竞争锁,假设没有其他竞争线程,那么 Thread-1 竞争成功,如下图所示:

        此时 Thread-0 的状态就被 park 阻塞了,如下图:

6.2 signal 流程 

        假设此时 Thread-1 要来唤醒 Thread-0,如下图:

        signal() 方法的代码如下所示,首先进行健康检查,看调用方法的线程是否为锁的持有者,即只有 owner 才可以唤醒其他线程,接下来查找条件变量列表中的第一个元素,查找完毕之后调用 doSignal() 方法。

        doSignal() 方法是取得等待队列中第一个 Node,即 Thread-0 所在 Node

        此时的状态图如下所示:

        获取完 Node 之后,调用 transferForSignal() 方法,将该 Node 加入 AQS 队列尾部,将 Thread-0 waitStatus 改为 0Thread-3 waitStatus 改为 -1

        接下来就是 Thread-1 释放锁,把 state 置为 0,设置 owner null,唤醒等待队列中的下一个元素了,整个流程结束。

  • 24
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

快乐的小三菊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值