深究AQS与各种同步组件

0.前言

0.1 为什么要学AQS

相信学过并发编程的同学对于AQS应该都不陌生,AQS全名叫做队列式的同步器AQS(AbstractQueuedSynchronizer),内部定义了很多锁相关的方法。

来看它的UML图和思维导图(这两张图来着公众号Hollis,这篇文章也很多借鉴):
在这里插入图片描述
在这里插入图片描述
由上面两张图可以看到,AQS几乎与大半个JUC有关。只要学会了AQS,锁(独占锁和共享锁、公平锁和非公平锁、重入锁和不可重入锁)、同步工具类都可以说不成问题了,甚至也可以自定义一个同步工具类出来。

0.2 怎么学AQS

先说一些所需的背景知识,学习AQS之前需要了解一个设计模式——模板模式,AQS正是使用了这个模式;对于一些锁的概念也需要了解:独占锁和共享锁、公平锁和非公平锁、重入锁和不可重入锁;对于同步工具类的一些基本使用也要知道。
在这里插入图片描述
AQS由三个部分组成:volatile int state、线程同步队列、节点Node,AQS的操作(加锁、解锁等)其实就是对这三部分的组装完成的。不同的组装逻辑就实现了不同的功能。下面会对这三部分做出解释。

下面的讲解对这些组装逻辑进行讲解,我把AQS的各种实现分成两种模式,一种是独占模式(独占锁)、一种是共享模式(共享锁)

那重入锁非重入锁和公平非公平锁呢?相比起上面两种,其实只是在实现过程中一小地方不同而已,就不具体分成一类了,当然也会讲到。

在这里插入图片描述
下面开始学习,讲解过程中也会贴一些源码出来,理解如果有误也欢迎大家一起讨论,这里即是一个分享也是做了一下笔记。


1.模板模式与AQS

没看过模板模式的小伙伴,可以看这个视频,5分钟就可以搞定,很棒的UP主:五分钟搞定模板模式。我们都知道,在模板模式里方法分成了三种,一种是规定好的模板方法,一种是子类需重写的自定义方法,还有一种是用来辅助其他两种方法的辅助方法,在AQS中我们也可以看到这三种方法的存在。

  • 模板方法
    在这里插入图片描述
  • 自定义方法
    在这里插入图片描述
  • 辅助方法(这里只是简单举几个例子而已,不仅仅只有这些,甚至可以说这些辅助方法也是AQS中的一大难点)
    • getState():获取state的值
    • setState(int newState):设置state的值
    • compareAndSetState(int expect,int update):使用CAS设置state的值

AQS的方法大概就分成这三类,如果不先了解模板模式,在学习过程中会很混乱。举一个例子来更好了解下AQS的实现,比如ReentrantLock中的AQS实现,打开ReentrantLock,看它的源码,可以发现一个类叫做Sync
在这里插入图片描述
在这里插入图片描述
这个类和AQS的关系就是具体实现子类和模板类的关系,ReentrantLock的锁的实现就是利用这个来实现,也可以去看看使用了AQS的同步工具类,它们的内部也都有这个类。

那这个类在ReentrantLock中又干了什么呢?我们可以点开ReentrantLocklock()方法,看一下它的源码,其实就是直接调用sync里的方法而已,ReentrantLock的其他API也一样,都是直接调用sync,到这里应该可以明白了吧。
在这里插入图片描述
其实AQS和Sync隐藏了锁的各种操作的具体细节,使用的时候直接调用即可。由此我们也可以得出一个结论:要想实现一个同步工具类(基于AQS),就要在内部建一个内部类,继承AQS,重写自定义方法。

问题:为什么要写成包装内部类的形式,再去调用呢,直接使用不香吗,即直接使用Sync类。

(其实问题是我自己当时想出来的,答案也是自己想的,没有标准答案)
答案:这样的设计更加优秀,具有可扩展性,拿ReentrantLock举例,它的sync类只是实现了独占锁(排他锁)的功能,但是它写成了内部类的形式,它可以再用两个内部类继承他,实现了公平锁和非公平锁,扩展了功能

这里也扩展一下,我们知道ReentrantLock有公平锁和非公平锁的功能,这个就是利用其余的两个Sync,其实名字已经写在那里了,它们都继承了Sync
在这里插入图片描述
在这里插入图片描述


2.AQS的实现

2.1 state

state其实很难用语言去描述它是什么,因为它在不同的实现逻辑里有不同的叫法,比如在ReentrantLock中表示当前锁是否存在,1表示锁被使用了,0表示锁没被使用。我个人把它叫做同步状态,来看下他的源码,其实就是一个简单的 int 类型:
在这里插入图片描述
这里简单总结下,AQS各种实现的state的所表示的逻辑:

在这里插入图片描述

2.2 同步队列

AQS中维护了一个FIFO队列,这个队列的每一个元素就是一个Node,Node包含了与其绑定的线程的信息,下面会讲到。这个队列使用的存储结构是双向链表。

在这里插入图片描述
这个队列是含有头节点的,头节点表示获得锁的线程,后面的节点是排队的节点。注意:只有第二个节点(前驱节点是头节点的节点)才会尝试去获取锁,其他节点都会被挂起。注意这个队列并不是直接使用Java提供的数据结构类Queue,而是由一个双向链表实现的。

2.3 Node

没有成功获得锁的线程将会被包装成一个节点加入同步队列,Node节点绑定了一个线程,以及包含了等待状态等信息,来看看它的属性:
在这里插入图片描述
看不懂没关系,先对它大概有个了解即可,知道它里面有什么东西就行。如果你仔细看了上面那个图,你会发现好像存在两种队列:一个就是2.2的同步队列,其实还有一个等待队列。

AQS里面也维护了等待队列。在Object的监视器模型上,一个对象拥有一个同步队列与一个等待队列,而AQS拥有一个同步队列和多个等待队列。

这个等待队列比较特殊,和我们要讲的加锁解锁原理关系不大,而是和Condition有关系,这里就简单说明有这个东西即可,不具体展开了,有兴趣可以看这篇博客:AQS中的同步队列和等待队列

在这里插入图片描述


2.5 说明

下面就会讲AQS的各种具体实现了,是会根据源码来讲,源码其实并不简单,第一次接触逻辑还是很混乱的,所以再讲解过程中我也会做流程图,来帮助大家更好理解。

还有一点,在看下面的内容之前,请先记住AQS的设计模式,记住哪些方法需要重写,哪些是模板方法,一般来说模板方法都是没有带try的,带try的一般都是自定义方法。熟悉了这些方法的作用后,对于下面看源码会帮助很大。

在讲解之前,我觉得有必要介绍一下一种代码风格,因为源码就是按这种风格写的,当时我第一次看源码的时候觉得这种风格有点难接受,一下子没反应过来(或许是我太菜了)。

先模拟一个场景:有三个方法A()、B()、C(),返回值都是布尔类型,它们的交互逻辑是,如果A返回false,则执行B,如果B返回true,则执行C

正常写法
if(!A()){
	if(B()){
		C();
	}
}

特殊写法
if(!A() && B() && C());

这样确实比较简略,可读性方面就见仁见智吧,刚开始可能不喜欢,反正我现在是喜欢上这种写法了,感觉挺高级的哈哈 -,-

还有一点就是我下面会一直讲到线程获取到了锁,这里的锁并不是真正意义上的锁,是我说顺口了,其实他可能就是改了state,当前获得同步状态的线程设置成当前线程。大家按自己意思理解即可。


3.独占模式——ReenrantLock的AQS实现

AQS有两种模式,独占模式和共享模式,所谓独占模式就是一次性只能给一个线程获得锁,或者说只能由一个线程执行,其他线程都阻塞。ReenrantLock正是这种锁,我习惯叫做独占锁,当然也叫做排他锁。

ReenrantLock也是可重入锁,所以它的AQS实现也是可重入锁的实现,同时ReenrantLock也有两种模式:非公平和公平,对应了非公平锁和公平锁的AQS实现,上面也有说过。

接下来将会演示两种锁的AQS实现:可重入的公平的独占锁和可重入的非公平的独占锁。

3.1 可重入的公平的独占锁

源码基本都在这几个类中反复横跳:AQS、ReenrantLock、Sync、FairSync
在这里插入图片描述

3.1.1 加锁
  1. 第一步

    首先我们先看加锁,查看ReenrantLock的源码的Lock()方法,是直接调用Synclock(),而这个是一个抽象方法,由于现在我们探讨的是公平锁,所以我们看FairSynclock()方法,这个方法又调用了AQS的acquire(),并传参数1。

    这个1是什么意思呢?看前面state在ReenrantLock应该可以大概知道什么意思了。

    "ReenrantLock的Lock方法"
    public void lock() {
            sync.lock();
        }
    
    "sync的lock方法"
    abstract void lock();
    
    "FairSync的lock方法"
    final void lock() {
                acquire(1);
            }
    

    流程图大概如下:
    在这里插入图片描述

  2. 第二步

    现在开始AQS源码了,坐稳了!进入AQS的acquire()方法,这方法是模板方法,作用是获取独占锁。它的内容就是我上面所说的那种代码风格了。

    第一步是!tryAcquire(arg),因为arg是参数1,所以也是!tryAcquire(1),这一步是尝试获得锁。
    第二步是使用addWaiter(Node node),这个方法作用是线程获取锁失败后(tryAcquire返回false)把线程封装成节点,加入同步队列。
    第三步是使用acquireQueued(addWaiter(NULL), 1)),作用是使用死循环,将节点绑定的线程进行自旋,或者根据封装好的节点的前驱节点的状态(上面Node提到的waitStatus)做出相应的处理,一种处理是把节点的线程挂起,一种是修改节点的位置,具体操作看下面。
    第四步是使用selfInterrupt(),这个方法使用前提是acquireQueued(addWaiter(NULL), 1))返回的线程中断结果是true,这个方法的作用是中断此线程。

    "AQS里的acquire方法,模板方法,作用是获得独占锁"
    public final void acquire(int arg) {
    		//执行顺序:
    		//1.!tryAcquire(1),尝试获取锁
    		//2.addWaiter(Node.EXCLUSIVE), 1),把线程封装成节点加入同步队列中
    		//3.acquireQueued(addWaiter(Node.EXCLUSIVE), 1))
    		//4.selfInterrupt()
            if (!tryAcquire(arg) &&
               acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                selfInterrupt();
        }
    
    "AQS的tryAcquire方法,自定义方法,需重写"
    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }
    
  • tryAcquire(int arg)

    tryAcquire(arg)就是我们需要重写的自定义方法。所以我们回到ReenrantLockFairSynctryAcquire(arg)方法。 这个方法的作用就是来尝试获得锁,它在FairSync中的实现逻辑是判断当前state是否等于0。

    如果不等于0,则表示锁被获取了,那再判断一下当前获得锁的线程是否等于本身的这个线程,这里是可重入锁的实现,如果等于则表示现在是锁的重入,设置state表示锁的重入次数。如果不等于该线程,则表示当前锁被别人占用了,直接返回fasle表示获得锁失败,执行下一步addWaiter(Node node)

    如果等于0,则表示当前锁还没人使用,这时会使用!hasQueuedPredecessors(),这个方法是辅助方法,作用是判断当前同步队列中是否有别的线程在排队,如果有则进行锁重入的判断;如果没有则使用CAS修改state,这里其实就是0变1,也即锁的获取,然后再把当前线程设置为获取了锁的线程,最后返回false表示线程获得所成功,这时不会进入下一步addWaiter(Node node)

    hasQueuedPredecessors()compareAndSetState(0, acquires)setExclusiveOwnerThread(current)这三个都是辅助方法,后面两个比较容易理解,hasQueuedPredecessors()比较复杂一点,这里也不展开了,这里有一篇博客写的也很不错:AQS-hasQueuedPredecessors()解析

    下面是tryAcquire的源码和流程图:

    "FairSync的tryAcquire方法,作用是尝试获取锁"
    protected final boolean tryAcquire(int acquires) {
    			//获取当前线程
                final Thread current = Thread.currentThread();
                //获取state
                int c = getState();
                //如果state=0,即锁没人使用
                if (c == 0) {
                	//执行顺序:
                	//1.!hasQueuedPredecessors(),判断同步队列中是否有线程在排队
                	//2.compareAndSetState(0, acquires),使用CAS修改state
                	//3.setExclusiveOwnerThread(current),把当前线程设置为获得锁的线程
                    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");
                    //设置state
                    setState(nextc);
                    return true;
                }
                //state!=0,即锁被使用了
                return false;
            }
    

    tryAcquire流程图

  • addWaiter(Node node)

    这个方法是辅助方法,定义在AQS中,我们可以去AQS里查看,这个方法的作用是把线程封装成一个节点,加入同步队列中。注意一下,使用这个方法的前提是tryAcquire()返回false,即获取锁失败。

    在调用这个方法的时候,传的参数是Node.EXCLUSIVE,这个是定义在AQS的Node里面的一个常数,表示独占模式的节点的意思,其实值为NULL,所以实际上调用的方法是addWaiter(NULL)

    这个方法做了什么事情呢?首先将当前线程封装成一个节点,然后把这个节点加入同步队列尾部,这个加入有两种实现方式,都比较简单,这里就无需像tryAcquire()那样大幅度展开了。

    addWaiter()里面也还含有其它两个辅助方法,分别是compareAndSetTail(pred, node)enq(node),第一个方法就是使用CAS把节点设置成尾节点。

    第二个方法也是同样效果,但是它使用的是死循环,即只有将节点设置成尾部节点才算成功,同时它也有初始化的效果,因为使用enq(node)的场景就是同步队列还未初始化。它的逻辑也比较简单易懂。

    "AQS中的addWaiter方法"
    private Node addWaiter(Node mode) {
    		//新建一个节点,绑定线程,mode = null
            Node node = new Node(Thread.currentThread(), mode);
            //获取尾节点
            Node pred = tail;
            //尾节点不为空,把当前节点加入同步队列尾部
            if (pred != null) {
            	//将当前节点的前驱节点设置为尾节点
                node.prev = pred;
                //使用CAS把当前节点设置为尾节点
                if (compareAndSetTail(pred, node)) {
                	//将旧的尾节点的后继节点设置为当前节点
                    pred.next = node;
                    //返回封装好的节点
                    return node;
                }
            }
            //尾节点为空,把当前节点加入同步队列尾部
            enq(node);
            //返回封装好的节点
            return node;
        }
        
    "AQS中Node的构造方法"
    Node(Thread thread, Node mode) {     
                this.nextWaiter = mode;
                this.thread = thread;
            }
    
    "AQS中定义的Node.EXCLUSIVE"
    static final Node EXCLUSIVE = null;
    
    "AQS中的enq方法"
    private Node enq(final Node node) {
    	//死循环
        for (;;) {
        	//获取尾节点
            Node t = tail;
            //尾节点为空,表示同步队列还未初始化,初始化队列
            if (t == null) {
            	//利用CAS设置同步队列的头
                if (compareAndSetHead(new Node()))
                	//并让尾节点指向头,此时队列只有头节点
                    tail = head;
            }
            //尾节点不为空,把当前节点放到尾节点的后继节点上
            else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }
    
    

    在这里插入图片描述

  • acquireQueued(Node node,int arg)

    这个方法也是AQS中的辅助方法,这个方法非常重要,作用是使用死循环,根据前驱节点状态,让线程去抢锁,对线程挂起或者修改节点的位置。有一说一,这个方法的逻辑有点点复杂。下面先介绍这个方法里面包含的其他几个辅助方法:

    • predecessor():这个方法比较简单,作用是获取当前节点的前驱节点

    • shouldParkAfterFailedAcquire(p, node)这个方法就是对前驱节点的状态进行操作,如果前驱节点状态是SINGAL,则把线程挂起;如果前驱节点状态是CANCELLED,则节点前移到状态不为CANCELLED的节点后面;如果前驱节点状态既不是CANCELLED也不是SINGAL,使用CAS把前驱节点状态改成SINGAL

      为什么要这么做呢?这一点很重要(⭐),在AQS中一旦加入了同步队列,线程就会被阻塞。那什么时候被唤醒呢?由前一个节点唤醒。所以选择好一个前置节点就很重要,否则这个节点要是已经退出等待的了,那你将永远没有醒来的一天。简单说,就是为了找个好爹,因为你还得依赖它来唤醒呢,如果前驱节点取消了排队, 找前驱节点的前驱节点做爹,往前遍历总能找到一个好爹的。

    • parkAndCheckInterrupt():这个方法的作用就是把线程挂起,线程会被阻塞,一旦被前置节点唤醒,就会检查当前线程是否已经被中断,返回线程的中断检查结果。

    • cancelAcquire(node):这个方法是把当前节点从队列中移除,会被移除的节点都是发生了异常且没拿到锁的节点,是不正常的节点,这个方法实现也挺复杂的,这里也不多展开了,有兴趣可以看这篇博客:cancelAcquire() 方法。

    接下来讲下这个方法的逻辑,因为逻辑比较复杂,这里会比较简略说一下,大家主要看着源码和流程图自己跟一遍即可。

    首先是初始化两个标记变量:一个是失败标记为true,代表意义是否线程发生异常(线程被挂起或者中断,继续对线程操作会发生异常),默认是会发生;另一个变量是中断标记,这个比较好理解,表示线程是否中断,默认为false,表示线程不会中断。

    进入死循环,获取当前节点的前驱节点,判断前驱节点是否为头节点,如果是则尝试获取锁,这一点在上面的同步队列那里有说过。如果获取锁成功,则把当前节点设置为头节点,并且把失败标志改为false,表示线程不会发生异常,返回中断结果。

    如果不是前驱节点或者获取锁失败,则对节点的前驱节点进行判断,然后做出相应操作,这个是shouldParkAfterFailedAcquire(p, node)parkAndCheckInterrupt()的逻辑,执行完成后回到开始,继续判断前驱节点是否为头节点。

    要退出这个死循环有两种方法:一种是线程获取到了锁,一种是tryAcquire()方法发生异常(具体是什么情况才会发生异常我不大清楚),注意一点:线程被中断是完全不会影响这个方法的,这个方法虽然涉及了对于中断的操作,但是完全没有做出相关处理,也就是说线程被中断了对抢锁结果无影响,但在另一个方法acquireInterruptibly()里就有对中断的处理,下面也会讲到

    退出后会根据失败标记进行失败操作的处理,这一步也很好理解,如果获取到了锁,那么就说明没失败所以不必执行失败操作(失败标记为false);如果是线程挂起或者中断发生异常退出的情况,则要执行获取锁失败的处理(失败标记为true),处理就是把当前节点从队列中移除。

    源码和流程图如下:

    "AQS中的acquireQueued方法,作用是使用死循环对节点线程进行自旋,或者根据前驱节点状态进行挂起或者修改节点的位置"
    final boolean acquireQueued(final Node node, int arg) {
    		//失败标记默认为true(标识线程被中断或者发生异常,这里的异常一般是线程被挂起后发生的)
            boolean failed = true;
            try {
            	//中断标记(标识线程是否被中断),默认未false
                boolean interrupted = false;
                //死循环
                for (;;) {
                	//获得当前节点的前驱节点
                    final Node p = node.predecessor();
                    //执行顺序:
                    //1.p==head 判断前驱节点是否为头节点
                    //2.tryAcquire(1) 尝试获取锁
                    //这里的作用是判断前驱节点是否为头节点,如果是接着尝试获得锁,获得锁成功,则把当前节点设置成头节点
                    if (p == head && tryAcquire(arg)) {
                    	//设置头节点
                        setHead(node);
                        //帮助垃圾回收,如果不懂垃圾回收什么意思可以不用理
                        p.next = null; 
                        //失败标记改为false
                        failed = false;
                        //返回中断标记
                        return interrupted;
                    }
                    //执行顺序:
                    //1.shouldParkAfterFailedAcquire(p, node),这方法作用是根据前驱节点状态做出一些操作,具体看源码和流程图
                    //2. parkAndCheckInterrupt(),作用是将线程挂起,并返回线程的中断检查结果
                    if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                        //中断标记标记为true
                        interrupted = true;
                }
            } finally {
            	//由于上面是死循环,所以只有两种情况才能到达这里:
            	//1.节点的线程获取到锁了
            	//2.节点的线程被中断或者发生了异常(被挂起)
            	//第一种情况不会执行下面的方法,因为它的failed是false
            	//第二种情况才会执行下面方法,这个方法的作用是把节点从同步队列中取消
                if (failed)
                    cancelAcquire(node);
            }
        }
    
    "AQS中的predecessor()方法,作用是获取前驱节点"
    final Node predecessor() throws NullPointerException {
                Node p = prev;
                if (p == null)
                    throw new NullPointerException();
                else
                    return p;
            }
    
    "AQS中的shouldParkAfterFailedAcquire()方法,作用是根据前驱节点的状态对当前节点,进行一些操作"
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    		//获取前驱节点的状态
            int ws = pred.waitStatus;
            //如果前驱节点是SIGNAL,返回true
            if (ws == Node.SIGNAL)
                return true;
            //如果前驱节点是cancelled,则把节点前移到状态不为cancelled的节点后
            if (ws > 0) {
                do {
                    node.prev = pred = pred.prev;
                } while (pred.waitStatus > 0);
                pred.next = node;
            }
            //如果前驱节点状态既不是cancelled也不是SIGNAL,则使用CAS修改前驱节点状态为SIGNAL 
            else {
                compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
            }
            return false;
        }
    
    "AQS的parkAndCheckInterrupt方法,作用是将线程挂起,并返回线程的中断检查结果"
    private final boolean parkAndCheckInterrupt() {
            LockSupport.park(this);
            return Thread.interrupted();
        }
    
    "AQS中的cancelAcquire方法,作用是把节点从队列中移出,下面不多讲解了"
    private void cancelAcquire(Node node) {
            // Ignore if node doesn't exist
            if (node == null)
                return;
    
            node.thread = null;
    
            // Skip cancelled predecessors
            Node pred = node.prev;
            while (pred.waitStatus > 0)
                node.prev = pred = pred.prev;
    
            // predNext is the apparent node to unsplice. CASes below will
            // fail if not, in which case, we lost race vs another cancel
            // or signal, so no further action is necessary.
            Node predNext = pred.next;
    
            // Can use unconditional write instead of CAS here.
            // After this atomic step, other Nodes can skip past us.
            // Before, we are free of interference from other threads.
            node.waitStatus = Node.CANCELLED;
    
            // If we are the tail, remove ourselves.
            if (node == tail && compareAndSetTail(node, pred)) {
                compareAndSetNext(pred, predNext, null);
            } else {
                // If successor needs signal, try to set pred's next-link
                // so it will get one. Otherwise wake it up to propagate.
                int ws;
                if (pred != head &&
                    ((ws = pred.waitStatus) == Node.SIGNAL ||
                     (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                    pred.thread != null) {
                    Node next = node.next;
                    if (next != null && next.waitStatus <= 0)
                        compareAndSetNext(pred, predNext, next);
                } else {
                    unparkSuccessor(node);
                }
    
                node.next = node; // help GC
            }
        }
    

在这里插入图片描述

  • selfInterrupt()

    这个方法也是一个辅助方法,这个方法很简单,执行的前提是acquireQueued()方法返回的线程中断结果是true,即线程中断了。这里对线程进行中断处理,中断处理可以看我的另一篇博客:深究线程状态及切换

    下面截取一小段中断方法讲解:
    在这里插入图片描述

    "AQS中的selfInterrupt方法,作用是中断线程"
    static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }
    

OK了,这就是独占式的获取锁的实现,文字的讲述可能会感觉混乱,所以当自己去学习的时候,一定要画流程图才会看得懂思路。其实上述的内容也涉及一下接下来要讲的东西,比如响应中断的加锁、超时加锁以及节点的状态。(独占模式下,只要认识SINGAL和INITIAL就OK了,CANCELLED不用管)

OKK,现在tryAcquire()方法讲完了,很恐怖对吧,不过也就只有这个比较复杂了,下面其实很大基础上只是一些小细节不一样而已,现在来看下这个方法的逻辑流程图:
在这里插入图片描述

3.1.2 解锁

解锁其实相比起加锁就简单很多了,如果你吃透了上面的加锁步骤,解锁是一点难度都没有。okk,现在我们来看一下解锁步骤,和加锁一样还是在一系列类中反复横跳。

首先是ReentrantLockunlock()方法,它调用了AQS的release()方法,然后AQS又调用了ReentrantLock重写的tryRelease()方法。具体流程图和源代码如下:
在这里插入图片描述

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

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

"ReentrantLock重写的方法"
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;
        }

明白了具体的调用逻辑后,现在就来具体看源码了,先来看一下ReentrantLock的Sync的tryRelease(),这个方法做了什么呢?看下面源码解析和流程图。

"参数是1,releases是1"
boolean tryRelease(int releases) {
			"计算state"
            int c = getState() - releases;
            "判断一下,如果持有锁的线程不是请求释放锁的线程,就让这个请求是否锁的线程有多远滚多远,不是你持有的还敢来申请释放"
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            "一个标志,这里其实就是和可重入有关的,表示是否释放锁"
            boolean free = false;
            "如果state等于0,则说明没有线程重入,释放绑定线程,并把标志置为ture"
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            "修改state"
            setState(c);
			"返回标志,如果有重入即state大于0,表示解锁失败,则返回false;如果没有重入即state等于0,表示解锁成功,返回true,"
            return free;
        }

在这里插入图片描述
下面我们来看下AQS中的release():这个方法调用了tryRelease(1)方法,上面我们分析到,tryRelease(1)返回的结果就是解锁的结果,如果返回的是false表示解锁失败,那么release()也直接返回false;如果解锁成功,则会进入那个if语句里。还记得我们在加锁的时候,我们把线程挂起了吗,现在解锁了就要把线程唤醒,这里面 有个辅助方法unparkSuccessor(),就是唤醒后置节点,如果后置节点其实是已经取消状态的没用节点,那就不必唤醒它们了,而是从队列中重新找一个适合的新节点用于唤醒。

具体看下源码分析和流程图:

"参数是1,arg是1"
public final boolean release(int arg) {
		// 解锁成功
        if (tryRelease(arg)) {
        	"获取头节点,此时的头节点即绑定了当前线程的节点"
            Node h = head;
            "判断一下,如果头节点不为空且头节点状态不等于0(初始状态),一般这个判断都会返回true;为0表示同步队列只有头节点一个,没有后置节点"
            if (h != null && h.waitStatus != 0)
            	"唤醒后置节点"
                unparkSuccessor(h);
            "返回解锁成功"
            return true;
        }
        "解锁失败"
        return false;
    }

"唤醒后置节点,这里的node是头节点"
private void unparkSuccessor(Node node) {
		"获取节点状态" 
        int ws = node.waitStatus;
        "节点状态小于0"
        if (ws < 0)
        	"使用cas修改状态为0"
            compareAndSetWaitStatus(node, ws, 0);
        "获取后置节点"
        Node s = node.next;
        "如果后置节点为空且状态大于0(CANCELLED),说明后置节点是无用节点,需要清理"
        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);
    }

在这里插入图片描述
okk,现在来总结下整个解锁流程,看图:
在这里插入图片描述

3.1.3 总结

总结一下吧。

在并发环境下,加锁和解锁需要以下三个部件的协调:

  • 锁状态。我们要知道锁是不是被别的线程占有了,这个就是 state 的作用,它为 0 的时候代表没有线程占有锁,可以去争抢这个锁,用 CAS 将 state 设为 1,如果 CAS 成功,说明抢到了锁,这样其他线程就抢不到了,如果锁重入的话,state进行 +1 就可以,解锁就是减 1,直到 state 又变为 0,代表释放锁,所以 lock() 和 unlock() 必须要配对啊。然后唤醒等待队列中的第一个线程,让其来占有锁。
  • 线程的阻塞和解除阻塞。AQS 中采用了 LockSupport.park(thread) 来挂起线程,用 unpark 来唤醒线程。
  • 阻塞队列。因为争抢锁的线程可能很多,但是只能有一个线程拿到锁,其他的线程都必须等待,这个时候就需要一个 queue 来管理这些线程,AQS 用的是一个 FIFO 的队列,就是一个链表,每个 node 都持有后继节点的引用。

下面属于回顾环节,用简单的示例来说一遍,如果上面的有些东西没看懂,这里还有一次帮助你理解的机会。

首先,第一个线程调用 reentrantLock.lock(),翻到最前面可以发现,tryAcquire(1) 直接就返回 true 了,结束。只是设置了 state=1,连 head 都没有初始化,更谈不上什么阻塞队列了。要是线程 1 调用 unlock() 了,才有线程 2 来,那世界就太太太平了,完全没有交集嘛,那我还要 AQS 干嘛。

如果线程 1 没有调用 unlock() 之前,线程 2 调用了 lock(), 想想会发生什么?

线程 2 会初始化 head【new Node()】,同时线程 2 也会插入到阻塞队列并挂起 (注意看这里是一个 for 循环,而且设置 head 和 tail 的部分是不 return 的,只有入队成功才会跳出循环)

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

首先,是线程 2 初始化 head 节点,此时 headtail, waitStatus0
在这里插入图片描述

然后线程 2 入队:

aqs-2

同时我们也要看此时节点的 waitStatus,我们知道 head 节点是线程 2 初始化的,此时的 waitStatus 没有设置, java 默认会设置为 0,但是到 shouldParkAfterFailedAcquire 这个方法的时候,线程 2 会把前驱节点,也就是 head 的waitStatus设置为 -1。

那线程 2 节点此时的 waitStatus 是多少呢,由于没有设置,所以是 0;

如果线程 3 此时再进来,直接插到线程 2 的后面就可以了,此时线程 3 的 waitStatus 是 0,到 shouldParkAfterFailedAcquire 方法的时候把前驱节点线程 2 的 waitStatus 设置为 -1。

aqs-3

这里可以简单说下 waitStatus 中 SIGNAL(-1) 状态的意思,Doug Lea 注释的是:代表后继节点需要被唤醒。也就是说这个 waitStatus 其实代表的不是自己的状态,而是后继节点的状态,我们知道,每个 node 在入队的时候,都会把前驱节点的状态改为 SIGNAL,然后阻塞,等待被前驱唤醒。这里涉及的是两个问题:有线程取消了排队、唤醒操作。其实本质是一样的,读者也可以顺着 “waitStatus代表后继节点的状态” 这种思路去看一遍源码。

3.2 可重入的非公平的独占锁


4.共享模式模式——ReenrantReadWriteLock的AQS实现


5.自定义AQS工具类

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值