ReentrantLock 源码解析(三)—— 加锁源码分析

前面说了AQS,也说了ReentrantLock 加锁解锁的基本逻辑,现在就详细剖析源码

AQS 的简单介绍

ReentrantLock 加锁、解锁的基本逻辑介绍

park/unpark 到底是怎么回事

加锁源码分析

先看下,加锁的流程图,有个具体的印象,再看源码

在这里插入图片描述

假设有ABCD四个线程,同时执行到加锁这行代码。前文说过,state是0,代表可抢锁。

        final void lock() {
            if (compareAndSetState(0, 1)) // 假设A执行此代码成功
                setExclusiveOwnerThread(Thread.currentThread()); // 标识哪个线程执有该锁
            else
                acquire(1);
        }

// 这里compareAndSetState(0, 1),就是进行CAS操作
    protected final boolean compareAndSetState(int expect, int update) {
        // unsafe类调用的是native方法
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
    
// 线程A拿到了锁就直接返回了,那线程B C D 就进入 acquire(1) 方法
    public final void acquire(int arg) {
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)){
             selfInterrupt();
        }
    }
  • 先说 tryAcquire(arg) 就是抢锁
		// tryAcquire 这个方法,非公平锁调用这个方法,传入参数是1,这个与可重入相关,待会再细说。
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }

        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState(); // 获取 state状态,
            if (c == 0) { // state是0,继续抢锁。刚进入lock 没抢到,现在可能锁已释放,有可能这次就抢成功了。
                if (compareAndSetState(0, acquires)) { // B C D 线程再次竞争拿锁
                    setExclusiveOwnerThread(current);
                    return true; // 拿到了锁,lock 方法就结束了。
                }
            }
            // 来抢锁的线程,本身就执有锁,说明是再次加锁,state 再加 1
            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; // 抢锁失败
        }
        

这个方法没有难理解的逻辑,不过,setState(nextc); 这个方法没有用 CAS,这里会不会是bug

明确的告诉你,不是,因为这里不存在并发

current == getExclusiveOwnerThread()

即当前线程是执有锁的线程,只可能有一个线程进入这人条件分支,不需要用CAS

这里就是ReentrantLock可重入的体现,state=0,指锁不属于任何线程,

当某线程首次抢到锁,state=1,

此线程未释放锁的情况下,再次抢到锁,state=2,

这种情况下,只有连续释放两次锁,其它线程才可能抢到该锁。

这就是 ReentrantLock 锁的可重入。
  • 详细说说 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

addWaiter(Node.EXCLUSIVE),入队,自旋能保证,一定入队成功

addWaiter(Node.EXCLUSIVE)  这个方法就是用自旋的方式,保证线程入队

    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode); // 线程与Node绑定
        Node pred = tail;
        if (pred != null) { // 队列已经初始化,直接将new 出来的node 放到队尾
            node.prev = pred;
            if (compareAndSetTail(pred, node)) { // CAS 设置队尾
                pred.next = node;
                return node;
            }
        }
        // 走到这里,说明队列未初始化,或者上面并发入队,入队失败了。
        enq(node); 
        return node;
    }

	// 自旋方式入队
    private Node enq(final Node node) {
        for (;;) { // 死循环,保证入队一定成功
            Node t = tail;
            if (t == null) { // 队尾是null,说明得初始化队列
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

还记得上文画的那个AQS的图么

还是刚刚的4个线程,假设此刻 A 还没有释放锁,那state一定不等于0,exclusiveOwnerThread记录的就是线程A,

当B C D线程都进入addWaiter方法时,图中 head==null , tail == null,

想入队就要先初始化,即 t == null那个分支的代码。

在这里插入图片描述
从原码来看,队列用了哨兵,初始化就是new一个node不与任何线程绑定,

如下图所示,这个就是头节点,thread==null,之后入队的,thread一定有值
在这里插入图片描述
若 B C D线程中 B线程进入if (t == null) { } 这段代码,就完成了初始化,

那 C D 线程进入else分支,

它们俩个,谁执行compareAndSetTail()成功,谁就入队,

假设是C入队成功(如下图),那 B D 线程进入下一轮循环,

在这里插入图片描述
那 B D 线程进入下一轮循环,若存在并发,失败那个得再循环入队一次,

若不存在并发,就顺次入队。但最终大概都是这个样子。

在这里插入图片描述

这个方法里的逻辑是巨复杂的,不太好理解。

    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; // 从队列中剔除,等待GC 回收
                    failed = false;
                    return interrupted; // lock 方法结束
                }
                if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) {
  	                  interrupted = true; 
                }
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

	// 设置头节点
    private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }

先解释下这个条件 p == head && tryAcquire(arg) 前驱节点是head,且此节点抢锁成功,

结合上面那个图,B C D 三个节点,只有 C 节点才可以 抢锁,不要问为什么,这是规矩

在这里插入图片描述
假设在 B C D 执行acquireQueued方法时,恰好A释放了锁,C 线程刚好抢到了锁,队列就变成上图的样子。

B D 线程进入shouldParkAfterFailedAcquire(p, node)   parkAndCheckInterrupt() 这两上方法。

原来代表C线程的node成为新的头节点(抢锁成功,就要出队)。

head 指向新的头节点,头节点thread 设置为null,

新旧头节点之间的指针去掉,旧的头节点等待GC回收。

在讲解shouldParkAfterFailedAcquire(p, node)parkAndCheckInterrupt() 这两个方法之前,

说点题外话。


B C D 三个节点,只有 C 节点才可以 抢锁,Why ? 大神Doug Lea 就是这么写的,你想咋地!

个人认为,这样设计,代码实现简单,线程都已经进入等待队列了,说明并发比较高,

抢锁就派个代表出去就行了,别的继续在队列中等,出去抢锁的线程多了,CPU有意见。

再说,去的再多,也是只能是一个抢到锁。

派谁去,当然是头节点,去掉代码实现简单易懂,去其中任意一个,代码更加复杂。

不是说非公平锁么,那队列里,排在前面的先出队列去抢锁,很公平啊,哪里来的非公平?


这是个好问题,公平与非公平锁,不是在这里体现的。

不管公平锁还是非公平锁,队列里,都是前面那个出队抢锁,没区别,

公平与非公平,主要差别是tryAcquire() 这个方法上,

公平锁,若队列不为空,没入队的线程不得抢锁,

非公平锁,若队列不为空,没入队的线程却可以抢锁,

这时后到的线程可能先抢到锁,即不公平。

言归正传

  • shouldParkAfterFailedAcquire(p, node) 判断是否要阻塞线程

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus; // 前驱节点的waitStatus
        if (ws == Node.SIGNAL) // Node.SIGNAL 表示 -1
            return true;
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

这段代码逻辑很复杂,一句话,前驱节点 waitStatus是-1,就返回true.

不过,今天讲 lock() unlock() ,waitStatus都是初始状态0。

之后的博客中,还会再次讲到这个方法。


结合上图,以线程 B为例  waitStatus状态都是 0,前驱节点是头节点,其waitStatus也是0

首次进入shouldParkAfterFailedAcquire方法,
执行compareAndSetWaitStatus(pred, ws, Node.SIGNAL)这段代码之后,前驱节点waitStatus=-1,
之后走下一行 return false, compareAndSetWaitStatus 方法结束。
那在方法acquireQueued()for (;;) {},第一次循环就结束了, B 没拿到锁

for (;;) {} 第二次循环开始,再次抢锁,
假设还没拿到,会第二次进入shouldParkAfterFailedAcquire()
此时B 线程的前驱节点,waitStatus=-1 该方法返回 true;(解锁时会讲到这一点)
那程序会走到 parkAndCheckInterrupt()这个方法里,阻塞线程

也就是说,经过两次循环,才会去阻塞线程,每次循环都会去抢锁的

Doug Lea 设计的真是好,发现这个线程需要阻塞,还要再给次抢锁的机会。

万一抢到了呢!真的是抢不到,再真正阻塞线程。

  • parkAndCheckInterrupt() 阻塞线程
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this); // 阻塞线程
        return Thread.interrupted(); // 清除线程中断标记
    }
题外话
	park()阻塞了线程,有两种途径可以唤醒该线程:1)被unpark()2)被interrupt()。
	
	Thread.interrupted()当且仅当 线程被阻断时返回true,它还会清除当前线程的中断标记位,

若要了解 unpark() interrupt() 唤醒有何不同,请参看我的另外一篇博客——park后是如何被唤醒的

至此,lock 调用的内层代码讲完了,再回头看下抢锁的总逻辑

// 线程A拿到了锁就直接返回了,那线程B C D 就进入 acquire(1) 方法
    public final void acquire(int arg) {
    	// 抢锁,直到抢到为止,没抢到就在acquireQueued一直自旋转。
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)){
             selfInterrupt(); // 如果线程被中断过,给线程打个标记
        }
    }

    static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }

上面这段代码很有意思,作为题外话说说,当然你可以不看,有点绕。最好别看,看了会看糊涂的。

想想这段代码 Thread.currentThread().interrupt(); 什么时候会执行?

只有tryAcquire(arg)返回 false 且 acquireQueued() 返回 true 的时候才可以。

再看下,acquireQueued()什么时候才会返回 true 呢? 咱再看下原码

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

接着上文说,B线程 在for (;;)中,第二次进入shouldParkAfterFailedAcquire(),返回true,
进入parkAndCheckInterrupt(),调用 LockSupport.park(this)后,
返回 return Thread.interrupted();   

而 Thread.interrupted() 结果是false,因为目前 B线程 未调用过 interrupt(),继续自旋,

若在自旋过程中,其它的代码调用了interrupt(),那么 parkAndCheckInterrupt()会返回 true,

interrupted = true; 会被执行。

那 B线程在自旋过程中 parkAndCheckInterrupt() 会清除掉中断标记,但 interrupted = true是不会。

最终在acquire() 方法里,会明确的知道拿到锁的线程,曾经是否被中断过,若中断过,会重新在线程上做标记。



还有一点要说的

线程B 在自旋过程中,第二次 for 循环,会调用LockSupport.park(this)方法,程序就暂停了,不会再占用CPU资源了。

当被unpark()interrupt()唤醒时,会接着自旋,要么拿到了锁,

要么重新调用LockSupport.park(this)方法,继续等待。

具体是被unpark()唤醒的,还是被interrupt()唤醒的,程序上做了区分。

区分不区分,lock(), unlock() 用不到,啥时候乃至,之后的博客会写。

番外篇:park, unpark 示例讲解

上篇: ReentrantLock 加锁解锁的基本逻辑

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值