ReentranLock中AQS讲解

1、简介

AbstractQueuedSynchronizer,简称AQS,为构建不同的同步组件(重入锁,读写锁,CountDownLatch等)提供了可扩展的基础框架,如下图所示。(主要用来解释公平锁和非公平锁

AQS以模板方法模式在内部定义了获取和释放同步状态的模板方法,并留下钩子函数供子类继承时进行扩展,由子类决定在获取和释放同步状态时的细节,从而实现满足自身功能特性的需求。除此之外,AQS通过内部的同步队列管理获取同步状态失败的线程,向实现者屏蔽了线程阻塞和唤醒的细节。

2、AQS的设计思想

1、AQS中同步等待队列的实现是一个带头尾指针的双向链表。

head:队列首元素线程结点

tail:队列尾线程结点

2、内部类Node设计

  • prev:指向前一个结点的指针
  • next:指向后一个结点的指针
  • thread:当前结点表示的线程,因为同步队列中的结点内部封装了之前竞争锁失败的线程,故而结点内部必然有一个对应线程实例的引用
  • waitStatus:对于重入锁而言,主要有3个值。
    • 0:初始化状态;
    • -1(SIGNAL):当前结点表示的线程在释放锁后需要唤醒后续节点的线程;
    • 1(CANCELLED):在同步队列中等待的线程等待超时或者被中断,取消继续等待。

如下图所示:'

为了接下来能够更好的理解加锁和解锁过程的源码,对该同步队列的特性进行简单的讲解:

  • 1.同步队列是个先进先出(FIFO)队列,获取锁失败的线程将构造结点并加入队列的尾部,并阻塞自己。如何才能线程安全的实现入队是后面讲解的重点,毕竟我们在讲锁的实现,这部分代码肯定是不能用锁的。
  • 2.队列首结点可以用来表示当前正获取锁的线程。
  • 3.当前线程释放锁后将尝试唤醒后续处结点中处于阻塞状态的线程。

这个同步队列是FIFO队列,也就是说先在队列中等待的线程将比后面的线程更早的得到锁。

3、AQS中的state属性

这是一个带volatile前缀的int值,是一个类似计数器的东西。在不同的同步组件中有不同的含义。以ReentrantLock为例,state可以用来表示该锁被线程重入的次数。

当state为0表示该锁不被任何线程持有;

当state为1表示线程恰好持有该锁1次(未重入);

当state大于1则表示锁被线程重入state次。

因为这是一个会被并发访问的量,为了防止出现可见性问题要用volatile进行修饰。

4、exclusiveOwnerThread

该属性存在AbstractQueuedSynchronizer父类中

如注释所言,这是在独占同步模式下标记持有同步状态线程的。ReentrantLock就是典型的独占同步模式,该变量用来标识锁被哪个线程持有。

3、AbstractQueuedSynchronizer源码解析

1、非公平锁

1、加锁

了解AQS的主要结构后,就可以开始进行ReentrantLock的源码解读了。由于非公平锁在实际开发中用的比较多,故以讲解非公平锁的源码为主。以下面这段对非公平锁使用的代码为例:

java 复制代码
public class NoFairLockTest {

    public static void main(String[] args) {
        //创建非公平锁
        ReentrantLock lock = new ReentrantLock(false);
        try {
            //加锁
            lock.lock();
            //模拟业务处理用时
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //释放锁
            lock.unlock();
        }
    }
}

1、非公平锁Lock方法

2、真正的加锁入口

java 复制代码
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

首先尝试快速获取锁,以cas的方式将state的值更新为1,只有当state的原值为0时更新才能成功,因为state在ReentrantLock的语境下等同于锁被线程重入的次数,这意味着只有当前锁未被任何线程持有时该动作才会返回成功。若获取锁成功,则将当前线程标记为持有锁的线程,然后整个加锁流程就结束了。

若获取锁失败,则执行acquire方法

3、获取锁失败acquire分支

该方法主要的逻辑都在if判断条件中,这里面有3个重要的方法

  • tryAcquire()
  • addWaiter()
  • acquireQueued()

这三个方法中分别封装了加锁流程中的主要处理逻辑,理解了这三个方法到底做了哪些事情,整个加锁流程就清晰了。

4、tryAcquire方法

tryAcquire是AQS中定义的钩子方法,如下所示,该方法默认会抛出异常,强制同步组件通过扩展AQS来实现同步功能的时候必须重写该方法,ReentrantLock在公平和非公平模式下对此有不同实现,非公平模式的实现如下:

底层调用了nonfairTryAcquire()从方法名上我们就可以知道这是非公平模式下尝试获取锁的方法,具体方法实现如下

这是非公平模式下获取锁的通用方法。它囊括了当前线程在尝试获取锁时的所有可能情况:

  • 1.当前锁未被任何线程持有(state=0),则以cas方式获取锁,若获取成功则设置exclusiveOwnerThread为当前线程,然后返回成功的结果;若cas失败,说明在得到state=0和cas获取锁之间有其他线程已经获取了锁,返回失败结果。
  • 2.若锁已经被当前线程获取(state>0,exclusiveOwnerThread为当前线程),则将锁的重入次数加1(state+1),然后返回成功结果。因为该线程之前已经获得了锁,所以这个累加操作不用同步。
  • 3.若当前锁已经被其他线程持有(state>0,exclusiveOwnerThread不为当前线程),则直接返回失败结果

因为我们用state来统计锁被线程重入的次数,所以当前线程尝试获取锁的操作是否成功可以简化为:state值是否成功累加1,是则尝试获取锁成功,否则尝试获取锁失败。

其实这里还可以思考一个问题:nonfairTryAcquire已经实现了一个囊括所有可能情况的尝试获取锁的方式,为何在刚进入lock方法时还要通过compareAndSetState(0, 1)去获取锁,毕竟后者只有在锁未被任何线程持有时才能执行成功,我们完全可以把compareAndSetState(0, 1)去掉,对最后的结果不会有任何影响。这种在进行通用逻辑处理之前针对某些特殊情况提前进行处理的方式在后面还会看到,一个直观的想法就是它能提升性能,而代价是牺牲一定的代码简洁性。

5、退回到上层的acquire方法

tryAcquire(arg)返回成功,则说明当前线程成功获取了锁(第一次获取或者重入),由取反和&&可知,整个流程到这结束,只有当前线程获取锁失败才会执行后面的判断。先来看addWaiter(Node.EXCLUSIVE)部分,这部分代码描述了当线程获取锁失败时如何安全的加入同步等待队列。这部分代码可以说是整个加锁流程源码的精华,充分体现了并发编程的艺术性。

6、获取锁失败的线程如何安全的加入同步队列:addWaiter()

首先创建了一个新节点,并将当前线程实例封装在其内部,之后我们直接看enq(node)方法就可以了,中间这部分逻辑在enq(node)中都有,之所以加上这部分"重复代码"和尝试获取锁时的"重复代码"一样,对某些特殊情况进行提前处理,牺牲一定的代码可读性换取性能提升。

这里有两个CAS操作:

  • compareAndSetHead(new Node()),CAS方式更新head指针,仅当原值为null时更新成功
  • compareAndSetTail(t, node),CAS方式更新tail指针,仅当原值为t时更新成功

外层的for循环保证了所有获取锁失败的线程经过失败重试后最后都能加入同步队列。队列为空时要进行特殊处理,这部分在if分句中。注意当前线程所在的结点不能直接插入空队列,因为阻塞的线程是由前驱结点进行唤醒的。故先要插入一个结点作为队列首元素,当锁释放时由它来唤醒后面被阻塞的线程,从逻辑上这个队列首元素也可以表示当前正获取锁的线程,虽然并不一定真实持有其线程实例。

首先通过new Node()创建一个空结点,然后以CAS方式让头指针指向该结点(该结点并非当前线程所在的结点),若该操作成功,则将尾指针也指向该结点。这部分的操作流程可以用下图表示

当队列不为空,则执行通用的入队逻辑,这部分在else分句中

首先当前线程所在的结点的前向指针pre指向当前线程认为的尾结点,源码中用t表示。然后以CAS的方式将尾指针指向当前结点,该操作仅当tail=t,即尾指针在进行CAS前未改变时成功。若CAS执行成功,则将原尾结点的后向指针next指向新的尾结点。整个过程如下图所示

7、线程加入同步队列后**acquireQueued**

这段代码主要的内容都在for循环中,这是一个死循环,主要有两个if分句构成。

第一个if分句中,当前线程首先会判断前驱结点是否是头结点,如果是则尝试获取锁,获取锁成功则会设置当前结点为头结点(更新头指针)。为什么必须前驱结点为头结点才尝试去获取锁?因为头结点表示当前正占有锁的线程,正常情况下该线程释放锁后会通知后面结点中阻塞的线程,阻塞线程被唤醒后去获取锁,这是我们希望看到的。然而还有一种情况,就是前驱结点取消了等待,此时当前线程也会被唤醒,这时候就不应该去获取锁,而是往前回溯一直找到一个没有取消等待的结点,然后将自身连接在它后面。一旦我们成功获取了锁并成功将自身设置为头结点,就会跳出for循环。否则就会执行第二个if分句:确保前驱结点的状态为SIGNAL,然后阻塞当前线程。

8、shouldParkAfterFailedAcquire方法

可以看到针对前驱结点pred的状态会进行不同的处理

  • 1.pred状态为SIGNAL,则返回true,表示要阻塞当前线程。
  • 2.pred状态为CANCELLED,则一直往队列头部回溯直到找到一个状态不为CANCELLED的结点,将当前节点node挂在这个结点的后面。
  • pred的状态为初始化状态,此时通过compareAndSetWaitStatus(pred, ws, Node.SIGNAL)方法将pred的状态改为SIGNAL。

9、parkAndCheckInterrupt

shouldParkAfterFailedAcquire返回true表示应该阻塞当前线程,则会执行parkAndCheckInterrupt方法,这个方法比较简单,底层调用了LockSupport来阻塞当前线程

该方法内部通过调用LockSupport的park方法来阻塞当前线程

概括的说,线程在同步队列中会尝试获取锁,失败则被阻塞,被唤醒后会不停的重复这个过程,直到线程真正持有了锁,并将自身结点置于队列头部。

ReentrantLock非公平模式下的加锁流程如下

2、解锁

java 复制代码
    public void unlock() {
        sync.release(1);
    }

释放锁的方法如下:

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

尝试释放锁方法:

java 复制代码
 protected final boolean tryRelease(int releases) {
            // 获取获得锁线程的状态,并且减1
            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;
        }
java 复制代码
private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        // 判断当前线程的waitStatus状态 为-1的时候才可以唤醒后续的线程
        int ws = node.waitStatus;
        if (ws < 0)
            // 更新状态为初始化状态
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        // 获取要唤醒的线程
        Node s = node.next;
        // 不满足条件的话从后先前找到第一个满足条件的 
        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);
    }

2、公平锁

1、加锁

java 复制代码
        final void lock() {
            acquire(1);
        }
java 复制代码
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&           // 1. 先尝试直接获取,如果tryAcquire(arg)返回true直接结束
        acquireQueued(                // 2. 获取失败,加入队列等待
            addWaiter(Node.EXCLUSIVE), arg))  // 3. 创建节点加入队列
        selfInterrupt();              // 4. 如果在等待中被中断,补上中断标记
}
java 复制代码
        protected final boolean tryAcquire(int acquires) {
            // 获取当前线程
final Thread current = Thread.currentThread();
            // 获取当前状态
            int c = getState();
            if (c == 0) {
                // 如果当前线程之前还有符合条件的话返回true,取反为false,结束,直接返回false
                // 如果当前线程之前没有的符合条件,即它是第一个的话,就让他获取锁,返回true,获取锁过程结束
                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;
        }
    }

判断是不是还有比当前线程符合条件的,如果有返回true,没有返回false

java 复制代码
    public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        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()返回false,利用cas在tryAcquire()尝试获取锁,获取锁返回

如果当前线程不是第一个满足条件的,hasQueuedPredecessors()返回true,tryAcquire不进行cas,直接返回false,然后进入acquireQueued(addWaiter(Node.EXCLUSIVE), arg))中等待机会

2、解锁

和非公平锁的解锁方式一样

相关推荐
rainbow68893 小时前
C++智能指针实战:从入门到精通
java·开发语言
HalvmånEver3 小时前
Linux:进程 vs 线程:资源共享与独占全解析(线程四)
java·linux·运维
qq_12498707533 小时前
基于springboot的竞赛团队组建与管理系统的设计与实现(源码+论文+部署+安装)
java·vue.js·spring boot·后端·信息可视化·毕业设计·计算机毕业设计
瑞雪兆丰年兮3 小时前
[从0开始学Java|第五天]Java循环高级综合练习
java·开发语言
清铎3 小时前
项目_Agent实战
开发语言·人工智能·深度学习·算法·机器学习
BoJerry7773 小时前
数据结构——单链表(不带头)【C】
c语言·开发语言·数据结构
J_liaty3 小时前
SpringBoot 自定义注解实现接口加解密:一套完整的多算法方案
java·spring boot·算法
m0_748708053 小时前
C++代码移植性设计
开发语言·c++·算法
Dr.Kun3 小时前
【鲲码园PsychoPy】Go/No-go范式
开发语言·后端·golang