Java多线程第九篇--ReentrantLock与AbstractQueuedSynchronizer的恩怨情仇

前面我们对Java的锁做了简单的介绍和分类,下面我们从一些源码的角度来看看JUC包中lock锁的经典实现ReentrantLock,并且看看AbstractQueuedSynchronizer(AQS,下面就以该简称叙述了)同步器的底层实现。

本篇内容较多,希望大家耐心看完,比较难,不是很好理解,如有错误的地方,望大家海涵。标题只是为了吸引眼球,但两者确实有着千丝万缕的关系。因为想要搞明白Java的重入锁ReentrantLock,你必须要明白AQS;要想搞明白AQS,ReentrantLock锁的辅助分析又必不可少,所以我就两个一起看了。

Lock和synchronized的区别

在我们之前学习synchronized时,我们也说到,在JDK1.6之前效率是很低的,但是和LOCK相比较,还是有着很大的差距;在1.6之后做了很大的优化和提升(锁优化升级),相比较而言,性能上和LOCK相差无几;

虽然synchronized隐式的帮助我们实现了我们想要的锁的功能(不用我们自己操心如何上锁、解锁),但是缺少了上锁和解锁的可操作性,导致一些问题的不可控,且它为独占式的锁,在真正的高并发场景是满足不了我们的需求的;而Lock支持中断和超时、还支持尝试机制获取锁,提高了可控性和可操作性; 且在JUC包下的有着很多锁的实现,可以在不同的需求场景应用合适的锁。

下面就跟着笔者,打开工具idea/eclipse来进入源码世界吧,具体源码位置如下图: image.png

从类关系UML图大概了解AQS和Lock的关系

图片1.png 从上图中我们可以看到,这里有两个顶级接口,Lock和Condition,上源码 image.png image.png Lock就是锁的顶级父类,定义了几个加锁和释放锁的方法,还有一个就是可以新建条件等待队列的方法newCondition();

Condition按照我的理解它代表某个条件,意思就是如果某个条件还没达成,就会建立一个等待条件达成的等待队列,这个等待队列里放的都是些等待这个条件可行的线程。观其定义的一些方法,就跟我们之前篇幅里讲wait/notify范式类似,其实他们的原理都是相通的。其中的await就相当于wait(),signal()/signalAll()相当于notify()/notifuAll()。我们后面都会去分析的。

再回到上面的UML图,刚刚我们只是简单介绍了下两个顶级父接口,这里我们还有一个抽象类AbstractOwnableSynchronizer,他是我们主角AQS的父类。如下 image.png 这个类中只定义了一个属性exclusiveOwnerThread(当前独占模式所有者,本质是一个线程,独占模式什么意思后续会说),其他什么也没有,为什么就这么一个属性要单独搞个父类呢,我们观其父类的子类其实是有两个,还有一个AbstractQueuedLongSynchronizer,其实这个类就是AbstractQueuedSynchronizer的一个补充实现,就是里面的资源state变量,一个是int型,还有一个是long型而已。。。一般情况下,用不到哈~ image.png

再继续,我们看我们的主角AbstractQueuedSynchronizer类,观其类名就应该知道,这个类大概就是抽象的同步队列,既然是队列,那意思就是说,我们这个类里面维护了一个队列,学过数据结构的我们都知道,队列的建立一般都需要节点Node,这时我们再看UML图,在我们AQS的内部确实实现了一个Node的类。 image.png 这个节点类呢其实就是用来包装线程的,他给出了进入队列的线程的一些额外属性,比如当前的状态,它前面的线程节点,后面的线程节点等等。由此我们看出,AQS它就是一个用来管理同步线程队列的一个类。

再继续,我们看到了Lock的一个实现类(另一个主角),重入锁ReentrantLock,观其内部,一共继承AQS实现了三个类,一个Sync,还有两个继承了Sync,分别是NonfairSync(非公平)和FairSync(公平),其实这两个类就是我们之前讲过的公平和非公平锁的体现了。 image.png

AQS源码定义简单介绍

抽象同步队列AQS,其内部就是维护了一个共享资源state和一个带有头和尾的双向的FIFO的队列。其大致结构图如下:

image.png image.png 在AQS中,提供了两种资源的占有方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。这里面就是独占锁和共享锁的具体实现了。

既然AQS是抽象类,那么基于AQS我们程序员可以实现自定义的同步器。在AQS的类注释中,就已经告诉了我们如何使用AQS,如下截图:其中红色框部分就是自定义同步器,它实现了tryAcquire和tryRelease两个方法。

image.png

根据上面的例子我们可以看到,实现不同的同步器(独占/共享||公平/非公平)其本质就是争用共享资源state的方式不一样而已。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体同步等待队列的维护管理(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。以下就是我们需要实现的方法: image.png

  • isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
  • tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
  • tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
  • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

简单预热

这边我们先简要说下另外一个主角ReentrantLock公平锁的流程,预热下:在初始未有线程来加锁前,state的初始值为0。这时线程A调用lock方法,就会调用公平同步器的tryAcquire实现来尝试占用资源并进行state+1操作,如果这时有其他线程来lock并tryAcquire尝试占有资源时,就会发现此时state不为0了,已经被A线程占有,那么就会直接返回失败false并进入同步队列park等待。直到线程A调用unlock释放锁,即调用tryRelease将state减去获取的资源数变为0,其他线程才能一个个的从同步队列中被唤醒尝试加锁。当然这里面,会出现A线程再次获取资源的时候,这时候就是state累加的过程,这里就体现了重入锁概念。并且这里需要我们注意的是,我们线程A尝试加锁了多少次,那我们就得必须手动解锁多少次,不然其他线程将永远获取不到资源。

AQS中Node类的介绍

image.png 这边我们主要看几个节点状态的含义:变量waitStatus则表示当前Node结点的等待状态,共有5种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE、0。

  • SIGNAL(-1) :表示该线程节点的后继线程节点正在被阻塞或者已经被阻塞
  • CANCELLED(1):因为超时或中断,该线程已经被取消,其前驱节点释放锁后不会让处于该种状态的线程去竞争锁资源
  • CONDITION(-2):表明该线程被处于条件队列,就是因为调用了Condition.await而被阻塞
  • PROPAGATE(-3):传播共享锁()
  • 0:0代表无状态,默认Node就是该种状态。也可以叫做初始状态,当存在后驱节点追加,就去把其前驱节点设置成SIGNAL

注意,负值表示结点处于有效等待状态,而正值表示结点已被取消。所以源码中很多地方用>0、<0来判断结点的状态是否正常。

这些状态的各个值到底是什么意思?不要着急,我们后面会分析的。

以上便是AQS源码的相关概念介绍了。下面正篇来了。

源码分析-ReentrantLock(独占式可重入公平锁)上锁过程(AQS同步队列)

先来个demo

public class LockDemo {
    //传值true,代表公平
    static ReentrantLock lock = new ReentrantLock(true);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 6; i++) {
            Thread t = new Thread("t" + i) {
                @Override
                public void run() {
                	System.out.println(Thread.currentThread().getName() +"来加锁,此时队列长度:"+lock.getQueueLength());
                    lock.lock();
                    System.out.println(Thread.currentThread().getName() + " lock success");
                    for (int j = 0; j < 3; j++) {
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                        System.out.println(Thread.currentThread().getName() + " 处理业务中..." + j);
                    }

                    lock.unlock();
                    System.out.println(Thread.currentThread().getName() + " unlock success");
                }
            };
            t.start();
            //睡眠1s,保证t0 t1 t2 t3 的执行顺序
            Thread.sleep(1000);
        }
    }
}

效果如下:t0是第一个加锁的线程,然后是t1,t2,t3...下面的源码分析,线程的名字就如此定义了,请注意不要打架~

t0来加锁,此时队列长度:0
t0 lock success
t1来加锁,此时队列长度:0
t0 处理业务中...0
t2来加锁,此时队列长度:1
t0 处理业务中...1
t3来加锁,此时队列长度:2
t0 处理业务中...2
t0 unlock success
t1 lock success
t4来加锁,此时队列长度:2
t1 处理业务中...0
t5来加锁,此时队列长度:3
t1 处理业务中...1
t1 处理业务中...2
t1 unlock success
t2 lock success
t2 处理业务中...0
t2 处理业务中...1
t2 处理业务中...2
t2 unlock success
t3 lock success
t3 处理业务中...0
t3 处理业务中...1
t3 处理业务中...2
t3 unlock success
t4 lock success
t4 处理业务中...0
t4 处理业务中...1
t4 处理业务中...2
t4 unlock success
t5 lock success
t5 处理业务中...0
t5 处理业务中...1
t5 处理业务中...2
t5 unlock success

demo场景效果说明:这是一段公平锁交替竞争加锁的过程,也是我们最简单的业务场景,我们就按照这样的场景进行上锁过程的分析。(PS:虽然场景是简单的,但我在下面具体分析的时候,可能也会穿插其他场景的。。。因为并发本身就是一个很复杂的事情,如果一上来就分析并发复杂场景,反而不利于我们分析源码。

&&和||

在正式分析代码前,我先贴个网上的截图哈~补充小白知识!!那就是短路与(&&)和短路或(||)操作 image.png 很重要,因为本片源码里太多的短路与和短路或操作了哈,这些就不用解释了。

AQS初始状态

首先我们先来看看AQS同步队列的初始状态结构如下图:即同步资源state=0,独占线程exclusiveOwnerThread为空,同步队列的头尾都为空。 image.png (PS:由于源代码调用过程有点绕,所以很多地方我就直接在代码进行注释分析了,请注意看注释~)

t0线程加锁

首先第一个线程来加锁,从方法 lock.lock()开始,我们通过打断点的方式,发现其调用路径是ReentrantLock.lock()->FairSync.lock()->AbstractQueuedSynchronizer.acquire(1),下面便是AbstractQueuedSynchronizer.acquire(1)的实现,标注为代码1

    //代码1
    //#AbstractQueuedSynchronizer.acquire()
    public final void acquire(int arg) {
        /**
        t0线程
            t0线程tryAcquire返回true(见代码2的注释),即!tryAcquire(arg)返回false,则不进入&&
            操作后续的方法了,t0线程执行到此结束,其实这时t0线程已经获取到了同步资源,并且将自己放
            入了队列的独占线程引用变量exclusiveOwnerThread里了
        */
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

它首先调用tryAcquire()方法,而我们通过上面的理论概念知道,tryAcquire是由ReentrantLock锁自己自定义实现的,于是跟踪代码,最终找到了ReentrantLock中公平同步器FairSync.tryAcquire(),实现如下:标注为代码2

    //代码2
    //FairSync.tryAcquire()
    protected final boolean tryAcquire(int acquires) {
            //获取当前线程,当前是t0来加锁,则此时current==t0
            final Thread current = Thread.currentThread();
            /**获取同步资源c,目前是第一个t0线程来获取,则此时c=0
             (PS:如果此时是t1线程来尝试加锁,调用这段代码,则此时C!=0,因为线程1将同步资源+1操作了)
                */
            int c = getState();
            if (c == 0) {
                /*
                t0线程来加锁
                    -->t0线程,一定会走到这里,调用hasQueuedPredecessors这个方法,见代码3的注释分析
                    -->判断完是否需要排队,!hasQueuedPredecessors()为true,
                    -->则进入compareAndSetState(0, acquires)方法,cas替换state值为1
                    -->紧接着执行setExclusiveOwnerThread(current),将当前线程赋值给独占线程
                    -->方法tryAcquire在此处返回true,那么就会再次回到代码1
                */
                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;
       }

下面是代码3

//代码3
//hasQueuedPredecessors.hasQueuedPredecessors()
public final boolean hasQueuedPredecessors() {
        /**
        这个方法看似简单,但其实相当的难以理解,我们可以先简化理解为此方法就是用来判断当前线程需不需要排
        队。
        首先我们来看t0线程过来调用此方法,首先同步队列在new新建出来的时候,队列的首尾指向的都是空
        节点,即表明此时同步队列为空,则此时下面的代码 h != t 即为false,由于后面是与操作,所以后面的
        代码不管怎么样,都不会往下执行了,直接返回false
        
        (其实这段代码看似简单,实则很复杂,因为他的情况实在是太多了,而且这边你难道就没有疑问吗,
        当前t0线程来加锁,他是第一个线程,没有其他线程来争抢资源,当然就没有其他线程在排队等待锁资源,
        为什么这边还要判断,这个我们后续会分析到,总之并发的场景只有你想不到的。。。)
        */
        Node t = tail; 
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
}

到此,如下图所示:t0线程加锁完成,此时state=1,exclusiveOwnerThread=t0,head tail依然为空。

image.png

其实这里面还有一个特殊场景,那就是如果t0线程在加锁成功后,又调用了一次lock方法会怎么样呢,其实它最终也会进入tryAcquire方法,只不过此时判断c!=0,会进入 【else if (current == getExclusiveOwnerThread())】 ,由于current和当前占有锁的都是t0,因此该判断返回true,由此会执行【int nextc = c + acquires;setState(nextc);return true;】state+1操作,然后返回true,即t0又进行了一次+1操作,即锁可以重入,这也是ReentrantLock重入锁的定义所在了。

t1线程加锁

场景描述如下:在t0加锁成功后,按照demo,过了1秒后,由于t0处理业务需要3秒,这时t1尝试加锁,其实是加不到锁的。 t1线程调用方法路径也是一样的,首先会进入到代码1,然后其实是先调用代码2。

再次引入代码2

    //代码2
    //FairSync.tryAcquire()
    protected final boolean tryAcquire(int acquires) {
            //这时t1来加锁,则current==t1
            final Thread current = Thread.currentThread();
            //获取同步资源c,此时c=1(因为t0获取了cas方式赋值了1),不会进【if (c == 0)】分支
            //又由于current为t1,而getExclusiveOwnerThread()=t0,
            //所以也不会进【else if (current == getExclusiveOwnerThread())】
            int c = getState();
            if (c == 0) {
                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;
            }
            //所以最终t1线程会走到这里,返回false,这时将会返回代码1
            return false;
       }

t1线程tryAcquire()返回false,进入到代码1,再次引入代码1

    //代码1
    //#AbstractQueuedSynchronizer.acquire()
    public final void acquire(int arg) {
        /**
        t1线程
           tryAcquire(arg)值为false,则!tryAcquire(arg)为true,进入到&&后面的
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法,这个方法首先会调用
                addWaiter(Node.EXCLUSIVE)方法,见代码4
        */
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

代码4,就是【addWaiter(Node.EXCLUSIVE)】的调用方法,AQS的私有方法,实现如下:

//代码4
//AQS.addWaiter
private Node addWaiter(Node mode) {
     //t1线程进来,mode为EXCLUSIVE,即为独占锁,首先通过如下构造函数,构造节点node
     //其实就是将t1线程进行节点包装,注意:node节点状态waitStatus=0,看Node的定义和构造器可知
        Node node = new Node(Thread.currentThread(), mode);
     //取出尾节点赋给pred引用变量,由上面的分析我们知道,尾节点此时为空,所以【pred != null】为false
     //将进入到方法【enq(node)】,见代码5
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
}

代码5,此时t1线程进入方法enq,此方法是一个自旋的过程,见下面的分析

 private Node enq(final Node node) {
         //此时传入的node即为t1线程包装的node
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                //构造头节点,其实也就是初始化队列
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                //将node节点,添加至队尾
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
}

enq(final Node node)方法自旋过程分析如下

  • (1)t1第一次进来,t=tail=null,进入【if (compareAndSetHead(new Node()))tail = head;】会new一个节点(但里面的thread为空,我就叫他为空节点),将这个空节点赋给head和tail,此时如下图所示:

image.png

  • (2)由于for死循环,t1再次进入for循环,此时head=tail=空节点,将进入else分支,将node节点放到队尾,如下示意图:其中红色的线条就代表将关系解除了,建立新的以虚线代表的关系,其中蓝色的字样就是代码执行的顺序。

image.png 这时最终返回t,此时的t为当前节点的前置节点(按照目前的场景其实就是head空节点,其实这时的返回值也没有用到,所以先不管)。 这时再次进入代码4,返回node(t1节点,此时是尾节点了),进入方法acquireQueued(addWaiter(Node.EXCLUSIVE), arg),即进入代码6

//代码6
//AQS.acquireQueued
final boolean acquireQueued(final Node node, int arg) {
        //这时传入的node即为t1节点,arg=1
        boolean failed = true;
        try {
            boolean interrupted = false;
            //cas又是一个死循环。。。
            for (;;) {
                final Node p = node.predecessor();
                /**取出当前线程的前置节点,如果当前线程节点t1前置节点是头节点,则它会再次尝试获取锁
                进入tryAcquire方法。。
                这里其实是一个cas尝试加锁的操作,为什么会有这样的操作呢?这是Doug Lea大神的牛逼之处
                了,因为这时很有可能出现一种情况就是,当t1节点调用tryAcquire方法的那一瞬间,t0线程正好
                释放锁资源了,那不就意味着其他线程可以尝试加锁了,这时t1线程再次调用tryAcquire方法
                就会拿到锁,而不需要排队,其实这也是性能优化之一了,cas的思想无处不在,这也是AQS性能
                高的原因了。
                
               但显然我们当下的场景中,t1是不可能尝试加锁成功的,它会进入下面的if分支中
                */
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                  //见代码7的分析 
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

代码6的补充说明分析:这段代码再次进入一次死循环,我们来慢慢分析:

  • t1节点第一次进入for循环,这时先取出t1节点的前置节点p,此时p其实就是空节点(也就是头节点),即p==head,你会发现,代码会再次进入tryAcquire方法,由此你可以发现,其实这里的for循环,也是一次cas尝试加锁。
  • 由我们的demo的特殊性可知,t1的再次尝试加锁必然是失败的,所以代码会进入到下面的if分支,即shouldParkAfterFailedAcquire(p, node)方法,这又是另一段代码,我们标注为代码7,看到这时我们就有点晕了,方法实在是太多了,没关系,我们继续。 (PS:这只是我们分析的场景之一,从我们上面的注释可以知道,t1线程也是有可能获取到锁资源的)
  • t1第一次进入shouldParkAfterFailedAcquire方法返回false(见代码7的分析),会直接返回,进入第二次循环;这时又会走一次上面的流程,也会再一次的尝试加锁,按照我们demo场景,还是会加锁失败,则会再次进入到代码7。再次判断的时候就会返回true了,这时就会继续执行下面的parkAndCheckInterrupt(),标注为代码8,最后的结果就是将当前线程t1阻塞了(见代码8的分析)。

代码7

//代码7
//AQS.shouldParkAfterFailedAcquire
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        /**
        按照上面的分析下来,这时pred为空节点,即为head,node为t1节点,其实此方法
        从方法名,也可以猜出来是干嘛的,在尝试加锁失败后是否应该进行park操作,这时
        看到park单词,你应该感到庆幸,因为你快要走到终点了(当然只是最简单场景的终点。。。)
        下面这段代码,由于调用此方法的方法里面是一个自旋的for循环的过程,所以我下面的分析前面加上数字:
        (1)第一次进入此方法:由上面的结构图可知,这时pred的ws=0,即不会走【if (ws == Node.SIGNAL)】
        进入第二个【if (ws > 0)】,很明显会走到最后一个else分支,执行
        【compareAndSetWaitStatus(pred, ws, Node.SIGNAL);】,这里面就是将空节点(head)的ws置为
        SIGNAL -1,然后返回false,即shouldParkAfterFailedAcquire(p, node)方法返回false,这时再次
        回到acquireQueued方法,会再次进入下一次的循环;
        (2)第二次进入此方法,这时pred的ws=-1,即会直接进入第一个if分支,返回true,进入到
        acquireQueued
        
        代码分析到这,节点node的状态waitStatus还是一个比较重要的属性,现在再次回忆下我们
        一开始介绍Node类的几个状态的介绍:
        0:0代表无状态,默认Node就是该种状态。也可以叫做初始状态
        SIGNAL(-1) :表示该线程节点的后继线程节点正在被阻塞或者已经被阻塞;
        注意,负值表示结点处于有效等待状态,而正值表示结点已被取消。所以源码中很多地方用>0、<0来
        判断结点的状态是否正常。
        这样分析的话,应该就是这么个意思哈~~如有不对,海涵,个人理解。。
        
        其实关于SIGNAL网上还有其他的一些解释,比如:表示后继结点在等待当前结点唤醒。后继结点入
        队时,会将前继结点的状态更新为SIGNAL
        
        结合以上还有本身Signal单词本身的意思我觉得还有个更加准确的理解:
        signal通知唤醒的意思,说明该节点线程被标记为我可以唤醒了,表示该线程可以被正式通知唤醒了,
        可以来通知我释放资源了,其实在释放锁唤醒后继线程的时候,也恰巧是这些状态为signal的
        节点,其实也能说明,将我置为signal的后继节点正在被阻塞或者已经被阻塞了。
        
        比较绕,我尽力了,希望各位能够理解,算是回答了文章一开始介绍Node类状态的问题~
        */
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            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;
}

代码8

 private final boolean parkAndCheckInterrupt() {
     //park当前线程,这边就是将当前线程t1阻塞了
        LockSupport.park(this);
        return Thread.interrupted();
    }

这时我们再看此时AQS的队列状况图如下:t1节点成功加入到同步队列,头节点状态为-1,t1节点状态为0;锁的状态为被t0持有,state=1,到此t1线程阻塞,t1流程结束。 image.png

t2线程加锁

按照我们demo的场景,最终t2会和t1一样,加锁失败,进入同步队列排队,最终效果如下图所示: image.png 下面简要说下此场景流程:

  • (1)t2尝试加锁失败,进入addWaiter方法,结果是t2加入到队尾
private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        Node pred = tail;
        //这时pred即尾节点不为空,则进入if分支,直接将t2节点加入到队尾返回node
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

(2)进入acquireQueued方法,会再次进行for循环,最终结果就是会将t1节点的状态改为-1,然后t2线程阻塞。

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
            /**
            此时t2节点的前置节点为t1,不是head节点,则直接进入shouldParkAfterFailedAcquire
            这时shouldParkAfterFailedAcquire处理过程和上面t1的分析差不多,第一次进入将前置节点t1的ws
            赋值-1;第二次进入的时候,直接返回true。
            */
                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);
        }
    }

t3加锁

按照demo的场景分析,t3来加锁的时候,此时其实和t2的加锁过程是一样的,按照demo的打印效果来看,当t3来加锁时,队列的长度为2

image.png image.png 我们再仔细研究下打印效果,我们可以看到,当t4来加锁时,按照我们上面的流程,队列长度不应该是3吗,为什么会打印2呢,而且我们可以看到在t4来加锁前,t0将锁释放了,那是不是在t0释放锁的同时,队列里是不是有节点出队了呢? 答案是肯定的,队列肯定是有进有出的。

独占公平锁加锁简单总结

以上便是最简单场景的加锁过程了,下面我结合了一本书将加锁过程画了一幅流程图(改了一点点),简单总结如下: image.png

  • 首先,addWaiter方法,先包装线程为节点node,然后判断tail是否为空,如果tail为空,说明此时的队列还未初始化,则先进行队列的初始化,进入到enp方法;如果tail不为空,则进行尾插入操作
    • enp方法:此方法是个自旋操作,首先判断尾节点是否为空,如果是空的,则new一个空节点作为头节点,顺便也是尾节点,这里正是初始化队列的过程;紧接着进行cas自旋操作直到成功插入尾节点为止(其实这里的操作和addWaiter的尾插入代码都是一样的)
  • 初始化好队列,并将节点成功放入队列后,执行acquireQueued,这个方法则是真正排队获取锁的过程。
    • 此方法也是一个自旋的过程,第一步判断当前节点的前置节点是否为head,是的话,就可以进行尝试加锁操作:这里说明了一个事情,只有前置节点为head的时候,当前节点才有资格去竞争锁资源。同时这个步骤也是自旋CAS加锁的过程
    • 如果不是head的话,则进入到shouldParkAfterFailedAcquire方法,此方法的逻辑是将当前节点的前置节点置为signal,即表示当前线程正在加入队列并阻塞的过程(也表示前面的节点你可以被唤醒释放了)
  • 设置成功后,进入parkAndCheckInterrupt进行阻塞等待(当然也有可能当前线程会尝试加锁成功,如果该线程正好是head的后继节点的话)

从此处的简单总结来看,我们加锁过程,其实是一个不断自旋尝试加锁,加入同步队列acquireQueued(这里也有自旋尝试加锁)的过程。

释放锁

我们先来看看简单场景t0释放锁,简单场景描述:t0获取锁处理业务后,t1成功加入同步队列,当t2成功加入到同步队列等待的同时,t0释放锁了,下面会怎么样呢? 我们通过打断点,最终可以看到,执行unlock方法,最终首先会进入AQS.release方法,我们看如下代码10

//代码10
//AQS.release
public final boolean release(int arg) {
        /**
        首先会调用tryRelease方法,这个方法同tryAcquire一样,需要lock自己实现,实现见代码11
        (1)当tryRelease返回true,说明尝试解锁成功
            如果此时头节点不为空的且头节点状态不为0的话,其实说明了一个问题,就是队列存在等待的线程节点
            这时调用unparkSuccessor,详见代码12
        */
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                //请注意,这边传参为头节点
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

代码11

//代码11
//此方法是公平锁和非公平锁的共有实现,是Sync的方法
//Sync.tryRelease
protected final boolean tryRelease(int releases) {
            /**
            既然是尝试释放锁,其实也就是将state操作进行对应减法操作
            首先 会将当前的state减去1
            判断如果当前释放锁的线程不是当前占有锁的线程,则抛出异常,这就不用讲了吧
            然后,判断当前的state是否为0
            如果为0,说明当前线程,解除完所有的锁了,则此时将独占锁的线程置为空
            如果不为0,则将新的state赋值进去
            这边之所以要判断0,因为锁资源会被累加,在本篇幅里,其实就是我上面讲过的重入锁
            同一个线程可以无数次的加锁,但是也要手动的一一对应的解锁。
            每次枷锁都+1,每次解锁都-1
            final void lock() {
                acquire(1);
            }
            public void unlock() {
                sync.release(1);
            }
            */
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            //返回true代表解锁成功,返回false说明当前线程还未完全解锁
            return free;
}

代码12

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.
         */
         /**
           此时node为头节点,此时head.ws = -1,则将head节点的ws,修改为0
           取出头节点的后置节点,按照场景分析,此时应该是t1,t1!=null,且t1节点的ws也不可能>0
           则此时直接进入【if (s != null)】分支,执行【LockSupport.unpark(s.thread);】
           将t1节点唤醒
         */
        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);
}

这时t0释放锁资源,唤醒t1,那么你觉得这时代码就到此结束了吗?不,还没有,我们再回过头来想想,唤醒t1是什么意思?哦,t1之前加入到队列被阻塞了,阻塞在哪一步来着?是不是下面这一段代码处? image.png

这个返回又是到哪里呢?请注意这边返回的是【Thread.interrupted()】,通过上面的代码跟踪,方法又回到acquireQueued了。那么这时Thread.interrupted()的返回值很关键,它返回值到底是什么呢?按照我们之前介绍Thread的时候介绍过,这方法用来测试当前线程是否已中断,如果被中断返回true,而且此方法清除线程的中断状态。 那我们这时的线程也没有被中断,那么它会返回false。 image.png 这时就会进入到【if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())】很显然会返回false,那么会干嘛呢,那么此时acquireQueued会继续进行自旋,这时取出t1的前置节点【final Node p = node.predecessor();】,t1的前置节点其实就是头节点,那么这时t1将会再次尝试获取锁资源,进入【tryAcquire(arg)】方法. image.png 这时由于t0释放锁了,所以state=0,将进入hasQueuedPredecessors()方法 image.png 以上判断无需排队(其实不用分析也知道,因为t1当前在排队的队列里呢,而且就是排队的第一个),则t1最终进入tryAcquire的加锁阶段,进入如下代码

image.png,最终返回true。

其实说真的,这里面真的有很多细节,比如为什么上面的代码修改state值为什么用compareAndSetState(0, acquires)这个方法,而不直接用setState呢? 这里我们先不管这个问题,因为代码还在继续,返回true,会继续进入到acquireQueued方法: image.png 首先执行setHead(node):此方法的作用就是将当前节点t1节点设置为头节点,由于头节点的特殊性,会同时将节点的线程和前置节点置为空。 image.png 其实这里将t1节点的线程置为空,感觉就是一种将t1线程从同步队列释放出来的感觉,也就是说t1线程拿到锁了,它可以做对应的业务了。这里举个生活中的例子:比如你去车站买票,如果窗口没人排队,你是不是就直接去买票了,如果这时你发现窗口有个人正在买票或者有人在排队,那么你就得去排队,那么这时正在窗口买票的是不是就相当于head节点呢,仔细想,是吧。那么下面这句话很重要:也就是说,在AQS里,其实真正持有当前锁的,恰恰是头节点对应的线程(PS:这边其实有点不严谨,一开始的t0不算置为空的,但你说t0不算,那t0也正好持有锁呢,所以也可以看作是这样子的),而且头节点的thread是空的,恰恰是持有锁的线程从队列里释放出来的证据,这也正好说明,在线程加入队列等待尝试获取锁的时候,也只有头节点的下一个节点,才有资格去尝试获取锁。

紧接着就是执行下面的代码了【p.next = null; // help GC failed = false;return interrupted;】。到此返回false,t1成功变为头节点并且拿到了锁,结束。

释放锁总结

我们刚才已经分析了简单场景t0释放锁的流程了,我们来看看此时的同步队列的结构是怎么样的呢? image.png 总结如下:

  • 在线程释放锁的同时,会唤醒它的后置节点
  • 后继节点被唤醒后,看自己有没有资格拿到锁资源(其实就是看前置节点是否为head),如果有资格,则尝试获取锁资源,并且将自己置为head

特殊场景

特殊场景:t0获取锁处理业务后,t1成功加入同步队列等待,当t2在尝试加锁,方法走到acquireQueued方法的时候,t0释放锁了,此时t2已经插入到尾节点了,这时t0释放锁,会怎么样呢?

其实这个场景很复杂,之所以复杂,因为是多线程并发的关系,并发!!!也就是说你在释放锁的时候,说不定我先拿到锁了。。这都是有可能的。再比如我们上面提出的一个问题:为什么上面的代码修改state值为什么用compareAndSetState(0, acquires)这个方法,而不直接用setState呢? 那是因为你在修改的同时,说不定此时已经被其他线程将state改为1了。所以很有可能尝试获取锁资源的时候,会失败的。。。

比如我们将上面的特殊场景用如下一幅图说明下(注意:这只是上面特殊场景其中一种情况而已),比较绕。。。 image.png 图说明:此刻t2刚加入队列,在t2进方法acquireQueued的刹那,这时t2已经插进尾部,且t2的状态还为0,且t1的状态也为0。这时,t0业务结束了,释放锁,且已经成功将state置为0,exclusiveOwnerThread等于null,并且唤醒t1节点,此时t1和t2将会同时执行下面的代码。 你觉得会怎么样呢?。。。 image.png 不要慌,我们来看,其实这里就是哪个线程先执行,如果t1先执行,则先进入tryAcquire方法尝试加锁,你说这时如果t2先执行,会怎么样呢,其实它不会执行tryAcquire,因为它前面还有个t1,不是头节点,但如果t0执行的比较快,在成功获取锁后,并且将t1自己设置为头节点后,t2才进来,那么这时,t2就有资格获取锁资源了,这时获取锁资源肯定也获取不到,因为这时已经被t1占用了,所以尝试失败,进入底下的shouldParkAfterFailedAcquire并进行下面的阻塞等待操作。其实上图中框出来代码就确保了,只有头部的下一个才能有资格尝试获取锁,而且获取锁的里面也必须得看有没有人排队,所以设计还是很巧妙的。

再看hasQueuedPredecessors方法

其实在tryAcquire的实现里,有个函数很重要,那就是hasQueuedPredecessors判断是否需要排队;这个方法返回false,不需要排队,意思就是直接当前线程可以去加锁;返回true,则不加锁,因为调用这个方法的取反了。下面就是该方法的实现,说到底就是一行代码。。。是不是很简单,但是理解起来很难。再看源码中对于返回值的注释:如果当前线程之前有一个排队的线程,则为True;如果当前线程在队列的开头或队列为空,则为false:意思就是如果当前线程在队列的开头(头节点的下一个,即第一个排队的线程)或者队列不存在,则当前线程可以去尝试加锁。

/**
* @return {@code true} if there is a queued thread preceding the
     *         current thread, and {@code false} if the current thread
     *         is at the head of the queue or the queue is empty
     翻译如下:如果当前线程之前有一个排队的线程,则为True;如果当前线程在队列的开头或队列为空,则为false
*/
public final boolean hasQueuedPredecessors() {
        Node t = tail; 
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
}
  • 首先是h!=t,判断头尾节点是否不一样,这里面有以下几种情况:内容很多,耐心看完。
    • 1、头尾节点都为空,代表空队列,null==null,所以h!=t返回false,由于&&,所以整个方法返回false,无需排队,其实这个就很好理解了,队列都没初始化,所以此时压根就没线程来加锁,无需排队~此时线程直接去加锁,但可能会失败,为社么呢?因为很有可能此时同时有两个线程都发现了这个情况,都去进行CAS修改state值,必然有一个失败,那失败的肯定就回去排队了。
    • 2、头尾节点都不为空,且头尾不一样,那么h!=t返回true,则此时就会执行&&后面的代码【((s = h.next) == null || s.thread != Thread.currentThread())】,这时先把h的后继节点给s,这时的s节点就代表是队列中第一个排队节点,为什么head节点不是队列第一个排队的呢,这个其实我们在之前的代码分析中说过,head节点要么是直接new的空节点(初始化的时候),要么是下面的节点顶上来的节点(也就是持有锁的时候),就好像你去排队买票,正在买票的人其实不在排队,但是他站在第一个位置,就相当于head节点。这时我们看(s = h.next) == null的结果:
      • 2.1、如果s==null代表队列中没有人在排队,所以返回true,由于后面是||操作,所以整体结果返回true,当前线程可以去CAS加锁了。
      • 2.2、如果s==null返回false,就代表队列中有人在排队,则再执行下面的【s.thread != Thread.currentThread()】,这句代码简单理解就是s节点(第一个排队节点)是不是就是当前来竞争锁的线程,也分为两种情况
        • 2.2.1、如果s.thread != Thread.currentThread()返回true,意思就是当前线程不是排队的第一个线程,那么(s = h.next) == null || s.thread != Thread.currentThread()就是 false||true = true,再与前面的h != t true进行&&操作,整体结果返回true,即当前线程需要排队,不会去CAS竞争锁。其实这个情况很合情合理,就比如买车票,当前有人在排队,且第一个排队的人和当前来买票的不是一个人,那么当前来的人肯定得排队哈~
        • 2.2.2、如果s.thread != Thread.currentThread()返回false,意思就是当前来竞争锁的线程恰好是排队的第一个线程。这个时候(s = h.next) == null || s.thread != Thread.currentThread()整体结果就是返回false,与前面的h != t true进行&&操作,那么最后就是false,所以不需要去排队,这时候当前线程进行CAS尝试加锁操作。为什么当前线程都已经排队了,为什么还能去进行尝试CAS操作,因为排队是排队了,但人家是第一个人,它可以通过CAS看看,持有锁的线程有没有释放啊,这个head节点的后继节点是有这个资格的。这也是大神代码的牛逼之处了。性能体现的淋漓尽致啊!!其实这种情况只有在很极限的时候才会出现。。。其实这时我们会思考,既然线程都已经在排队了,说明什么,说明线程已经park,怎么可能还能来竞争锁呢?首先我们看一个事情,在AQS和重入锁的加锁过程的实现代码中,hasQueuedPredecessors是在哪里被调用的,我们发现仅仅是在公平锁的tryAcquire中被调用的,而我们再看tryAcquire是在哪里被调用的,我们可以看到在两个地方被调用,一个是一开始的acquire还有一个就是acquireQueued,也就是说当线程在执行addwaiter后,进入acquireQueued又进行了一次tryAcquire,而且此时的tryAcquire是在一个for循环(自旋)里。 AQS理解很难的原因就是要结合不同的场景来分析,就比如下面两个场景叙述:
          • t0线程占有锁,t1来竞争锁,第一次调用tryAcquire失败,然后t1执行addwaiter包装节点,进入队列,这时t1就是队列的第一个了,这时线程还没park,进入acquireQueued的方法,此时t1正好是head的下一个,这时t1会再次tryAcquire,这时会再次进入hasQueuedPredecessors,这时就正好出现了当前所讨论的场景了。
          • 当t1再次tryAcquire失败后,t1就会正式进行park操作阻塞等待,这时t0业务结束了,释放锁,此时会去进行唤醒操作,必然唤醒的是t1线程,在被唤醒后,t1又将重新进入acquireQueued的循环中,此时t1又正好是head节点的下一个,那么t1又会再次进入tryAcquire,又会再次进入hasQueuedPredecessors。这时如果不判断s.thread != Thread.currentThread(),那么就会出现一个问题,h != t &&((s = h.next) == null)返回true,就让当前t1又去排队了。。。所以s.thread != Thread.currentThread()能够防止t1进入死循环。。。
        • 其实也印证了此方法的返回值的注释:如果当前线程之前有一个排队的线程,则为True;如果当前线程在队列的开头或队列为空,则为false。
    • 头尾节点都不为空,但头尾节点相等,其实此时队列也是空队列(至少AQS的api函数getQueueLength返回0),那么h != t返回false,那么直接整体返回false,不需要排队,当前线程可以去CAS尝试获取锁了。这里的场景就不叙述了,大家可以自己思考哈~

到此,hasQueuedPredecessors分析完毕..

中断相关

继续。。大家可能有个疑问就是,acquireQueued方法中 【boolean interrupted = false;】有什么作用,貌似我们的分析中至始至终都没分析它如下代码:

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

 private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
}

static void selfInterrupt() {
    //这里的意思就是直接将当前线程中断
   Thread.currentThread().interrupt();
}

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

我们在分析释放锁流程的时候,唤醒下一个节点比如t1的时候,说过,返回Thread.interrupted()为false,代表没有中断,此处并且清除了终端标记位,然后就又一次的进入循环,然后t1被设置为头节点,然后返回interrupted = false。紧接着返回到acquire方法,此时!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg)返回false,就不会执行selfInterrupt().

如果当前线程被中断了,会怎么样呢?唤醒下一个节点比如t1的时候,返回Thread.interrupted()为true,这时到acquireQueued方法,会将变量interrupted改为true后,再次进入循环,CAS尝试加锁成功后,设置为头节点,这时注意最终acquireQueued方法返回true,这时返回到acquire方法,此时!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg)返回true,执行selfInterrupt(),中断自己。

这里就可以看出来AQS的设计巧妙之处了,线程在队列里等待,但是却被意外中断,再次被唤醒后从队列出来继续执行的时候,就立刻中断了,保留了中断状态。

acquireInterruptibly方法(可中断式获取锁)

在lock中,我们知道除了可以lock方法外,我们还可以调用lock.lockInterruptibly(),可响应中断式锁。下面我们简要看下源码分析:最终会调用到AQS的acquireInterruptibly方法,如下:

 public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
}

在尝试获取锁失败后,就会进入到doAcquireInterruptibly(arg)方法,源码实现如下:

//doAcquireInterruptibly实现
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())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
}


//用来对比的
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
private Node addWaiter(Node mode) {
        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;
            }
        }
        enq(node);
        return node;
}
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);
        }
}

其实在我们分析完之前的加锁过程后,再来看doAcquireInterruptibly的源码,我们可以发现,其实是一样的,但有些地方不一样,就是少了boolean interrupted = false变量,而且在parkAndCheckInterrupt返回true时即线程阻塞时该线程被中断,代码抛出被中断异常。这里就体现出了可中断式锁的点了。

其他加锁方式

比如超时等待式获取锁(tryAcquireNanos()方法,其实你通过分析源码之后,最终会大概有三种情况:

  • 在超时时间内,当前线程成功获取了锁;
  • 当前线程在超时时间内被中断;
  • 超时时间结束,仍未获得锁返回false

非公平锁

以上都是通过公平锁来进入分析,现在我们再来看看非公平锁,其实这个在之前的篇幅里已经说过,其实公平锁和非公平锁主要的区别就是一个地方,在尝试获取锁之前有没有调用 是否需要排队方法hasQueuedPredecessors image.png image.png

其他的流程几乎一致

非公平锁VS公平锁

  • 公平锁每次获取到锁为同步队列中的第一个节点,保证请求资源时间上的绝对顺序,而非公平锁有可能刚释放锁的线程下次继续获取该锁,则有可能导致其他线程永远无法获取到锁,造成“饥饿”现象

  • 公平锁为了保证时间上的绝对顺序,需要频繁的上下文切换,而非公平锁会降低一定的上下文切换,降低性能开销。因此,ReentrantLock默认选择的是非公平锁,则是为了减少一部分上下文切换,保证了系统更大的吞吐量

重入性

image.png

以上便是AQS同步队列的源码分析了,其实AQS还有很多其他的实现源码没有分析,比如共享锁等等。

在AQS里除了维护了一个同步队列,还有一个叫做条件等待队列的维护,但其实实质也并非是AQS本身维护的,但也有着千丝万缕的联系。本来不想放在本篇文章里说的,但是想来想去,还是趁热打铁,继续撸它。

Condition条件等待队列的管理

我们回到本篇文章的一开始,我们看了AQS和Lock的UML关系图,那里面其实就已经提到了条件等待队列,我们通过源码分析知道在lock里new一个Condition,而这个方法实际上是会new出一个ConditionObject对象,这个对象的类实现其实是AQS的一个内部类,如下: image.png image.png 在AQS中,维护了一个同步队列,同样的原理,在ConditionObject的内部也维护了一个队列,即等待队列。然后,所有调用condition.await()方法的线程将会放到等待队列里。上面两幅图中框出来的最大的区域就是等待队列维护的具体实现了。

在分析前,我们先看下Node类的其中一个属性【Node nextWaiter;】学过数据结构的我们知道,其实从这个变量我们可以猜出,条件等待队列和同步队列不一样的地方是,同步队列的节点里,有next和prev,是一个典型的双向队列,而等待队列只有一个nextWaiter,是一个明显的单向队列。

还有就是Node的另一个状态CONDITION:

和上面一样,先上demo:

public class LockConditionDemo {
	static ReentrantLock lock = new ReentrantLock(true);
	static Condition condition = lock.newCondition();

	public static void main(String[] args) throws InterruptedException {
		for (int i = 0; i < 5; i++) {
			Thread thread = new Thread(() -> {
				lock.lock();
				System.out.println(Thread.currentThread().getName() + "上锁");
				try {
					System.out.println(Thread.currentThread().getName() + "等待...");
					condition.await();
				} catch (InterruptedException e) {
					e.printStackTrace();
				} finally {
					lock.unlock();
				}
			});
			thread.start();
			Thread.sleep(500);
		}
	}
}

运行效果:

Thread-0上锁
Thread-0等待...
Thread-1上锁
Thread-1等待...
Thread-2上锁
Thread-2等待...
Thread-3上锁
Thread-3等待...
Thread-4上锁
Thread-4等待...

demo场景描述:这是一段很烂的代码,但是能够让我们进入到我们想要的结果状态,通过运行我们看到上面的效果,你是不是觉得很奇怪,t0上锁了,而且已经等待了,为何没有park,下面的线程还能继续lock? 我们带着这样的疑问来看下condition是如何创建等待队列的。同样的,线程通过t0 t1 t2..这样子编号。

加入等待队列源码分析

先执行【lock.lock()】,这个过程我们就不分析了,直接看重点,执行【condition.await()】,就会直接进ConditionObject.await()方法,代码20如下:

//代码20
//ConditionObject.await
public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            //1 将当前线程包装等待节点,并且通过尾插入的方式加入到等待队列中,详情见代码21的分析
            Node node = addConditionWaiter();
            /*2 注意:该方法其实是AQS的一个final方法,目的是将当前线程所占用的lock锁资源释放,
            并且唤醒AQS中同步队列的后继节点,详情见见代码22分析
            其实这里也就回答了场景描述的带有的问题,因为会释放同部资源锁。。并且唤醒下一个节点t1
            t1就可以继续执行lock方法
            */
            long savedState = fullyRelease(node);
            int interruptMode = 0;
            /**
            注意:此段代码是一个带有条件的循环
            首先进行【!isOnSyncQueue(node)】判断,见代码23的分析,此时返回false,则成功进入while
            结构体里执行park,这时当前线程就会被阻塞了,当前线程也就运行到此了
            那当前线程怎么从await退出呢?
            按照代码的理解只有执行到【if(interruptMode = checkInterruptWhileWaiting(node)) != 0)】
            成立的话,或者【!isOnSyncQueue(node)】为false,即【isOnSyncQueue(node)】为true
            才会出循环。
            第一种的情况意思就是当前等待的线程被中断了,然后走到break,会退出while循环
            第二种的情况就是当前节点又被移动到了AQS同步队列中,其实就是有其他线程调用signal/signalAll
            方法。见代码23分析
            */
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
             //private static final int REINTERRUPT =  1;
            //private static final int THROW_IE    = -1;
            /**
            当退出while循环后,就会调用【acquireQueued(node, savedState)】再次将之前释放的锁资源
            都通过CAS的方式加回去,其实也就是再次尝试获取锁,直至成功。
            acquireQueued前面已经分析过了,这里就不再叙述了。返回的结果如果线程没有被中断,则为
            false,如果被中断则为true
               如果为true,则继续判断interruptMode 0 != THROW_IE -1 肯定也为true  则整体为true,
               则会执行【interruptMode = REINTERRUPT;】,即interruptMode = 1
               紧接着到下面【if (interruptMode != 0)】就会为true,则就会执行
               reportInterruptAfterWait(interruptMode)方法,这个方法就是用来处理被中断的线程的。
               这里的思想几乎和同步队列在被唤醒后的处理方式是一样的。如下面方法的具体实现,此时
               interruptMode = 1(REINTERRUPT)
               private void reportInterruptAfterWait(int interruptMode)
                        throws InterruptedException {
                    if (interruptMode == THROW_IE)
                            throw new InterruptedException();
                    else if (interruptMode == REINTERRUPT)
                    //很明显此时会走到这里
                            selfInterrupt();
               }
            
            走到这里差不多就知道意思了,当某个条件达到了,调用了signal方法(或者线程被中断),这时
            就会从等待队列回归到AQS的同步队列,进行再次的竞争锁资源,从而处理对应的业务。
            */
            if (acquireQueued(node, savedState) && interruptMode !=THROW_IE)
                interruptMode = REINTERRUPT;
            //处理取消的节点的代码,可自行查看,这里就不叙述了,方法的大致意思就是遍历等待队列
            //所有的节点,如果状态不是CONDITION的,则会被清理
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
}

代码21

private Node addConditionWaiter() {
            /**
            如果是t0进来
                首先取出等待队列的lastWaiter,其实这时候的等待队列还是空的,因此t==null
                所以会直接通过构造器new一个属于t0线程的等待节点node,且状态为CONDITION -2
                紧接着 t==null明显成立,则将node赋给firstWaiter,并且将node赋给lastWaiter
            看到这边,其实第一个即将等待的t0进入这里,就是进行队列的初始化,并且t0节点成功成为第一个
            节点。
            
            如果是t1进来
                t != null true  &&  t.waitStatus != Node.CONDITION false 第一个分支false
                则也一样,封装t1成等待节点
                【if (t == null)】不成立,进入else【t.nextWaiter = node;】,将当前t1的节点node置为
                尾节点的下一个节点,然后再把node赋值给lastwaiter,这是什么操作呢?
                其实这就是一个尾插入的过程
            
            如果是t2 t3 t4...进来,其实和t1一样了
            
            分析到这,其实此方法的目的就是封装线程为等待节点,如果队列未初始化,首先进行初始化,并且
            通过尾插入的方法,将节点放到队列中去,从这里也可以看出,等待节点的第一个节点(头节点),其
            实也就是等待的第一个(t0)节点,这点和AQS的同步队列不同,同步队列的head是不排队的。
            */
            Node t = lastWaiter;
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            if (t == null)
                firstWaiter = node;
            else
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
}

代码22

//代码22
//AQS.fullyRelease
final long fullyRelease(Node node) {
        boolean failed = true;
        try {
            long savedState = getState();
            //这段代码就不需要说明了吧,就是释放锁,并且唤醒同步队列下一个节点的过程
            if (release(savedState)) {
                //成功释放锁,并且返回释放锁资源的个数
                failed = false;
                return savedState;
            } else {
                throw new IllegalMonitorStateException();
            }
        } finally {
            if (failed)
                node.waitStatus = Node.CANCELLED;
        }
}

代码23

//代码23
//AQS.isOnSyncQueue 和AQS.findNodeFromTail
final boolean isOnSyncQueue(Node node) {
        /*
        此段if分支意思是,判断当前线程的状态是否为CONDITION,或者当前线程的前置节点是否为空
        判断当前线程的前置节点为空,说明什么?说明当前线程节点是等待队列的第一个节点
        
        很明显,大多数情况下,该方法执行到这就会直接返回false了,因为刚加入等待队列,节点状态
        为CONDITION
        
        当当前节点的状态不为CONDITION了(其实也就是被signal了后改变状态了),并且当前节点不是
        等待队列的第一个节点时,就不会进入该分支。
        */
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
        if (node.next != null) // If has successor, it must be on queue
            return true;
        /*
         * node.prev can be non-null, but not yet on queue because
         * the CAS to place it on queue can fail. So we have to
         * traverse from tail to make sure it actually made it.  It
         * will always be near the tail in calls to this method, and
         * unless the CAS failed (which is unlikely), it will be
         * there, so we hardly ever traverse much.
         */
        return findNodeFromTail(node);
        /**
        其实这段代码的大概意思就是,判断当前线程节点有没有被移动到AQS的同步队列中
        潜在意思就是,当调用condition.signal/condition.signalAll方法当前节点移动到了同步队列后
        就会返回true,暂且先这么理解吧。。
        */
    }

    /**
     * Returns true if node is on sync queue by searching backwards from tail
     * 翻译下:如果从同步队列的尾部向前查找,如果找到了在同步队列中,则返回true
     * Called only when needed by isOnSyncQueue.
     * @return true if present
     */
    private boolean findNodeFromTail(Node node) {
        Node t = tail;
        for (;;) {
            if (t == node)
                return true;
            if (t == null)
                return false;
            t = t.prev;
        }
    }

根据上面代码的注释分析,我们应该很清楚await的流程了。

其他方式的等待

awaitUninterruptibly 不响应中断的等待:看源码就可以直到,其实就是减少了对中断的处理,只要被中断了,就直接selfInterrupt()

public final void awaitUninterruptibly() {
            Node node = addConditionWaiter();
            long savedState = fullyRelease(node);
            boolean interrupted = false;
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if (Thread.interrupted())
                    interrupted = true;
            }
            if (acquireQueued(node, savedState) || interrupted)
                //直接中断,不抛异常了
                selfInterrupt();
}

await(long time, TimeUnit unit) awaitUntil(Date deadline) 超时机制的等待:其实实现的方式就是增加了时间的限制和处理,其他流程几乎一样,有兴趣的可以自己研究,这里就不再叙述了。

await总结

老一套,上结构图: image.png 总结:当当前线程调用condition.await后,会首先释放当前线程的锁,并从同步队列中封装成等待节点进入等待队列(也只能是头节点才能释放锁,见释放锁的流程);直至当前线程被signal从等待队列移动到同步队列中,直到当前线程重新尝试获取到了锁资源后(或者线程被中断),才会从await方法中返回。

到此,await的流程结束,在上面的分析过程中,一直提到signal后,节点会从等待队列移动到同步队列,下面我们来看看signal/signalAll的实现了。

signal/signalAll分析

一样,直接看源码,我们看调用condition.signal之后,会是怎样的流程呢?如下:ConditionObject.signal()->ConditionObject.doSignal(first) 首先代码24

//代码24
//ConditionObject.signal()
 public final void signal() {
         //首先检查当前线程有没有获取锁,没有的话,将会抛出异常
         //也就是说,当有个线程调用调用signal方法前,必须执行lock.lock()方法
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            //取出等待队列第一个节点
            Node first = firstWaiter;
            if (first != null)
                //通知第一个节点(也是最早进入等待队列的节点,等待时间最长),具体看下面的函数分析
                doSignal(first);
}

//ConditionObject.doSignal(first)
 private void doSignal(Node first) {
         /**
         
         */
            do {
                //此段判断意思就是队列空了。。直接将对头队尾都置为空
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                 //断开当前头节点与后继节点的链,就是将头节点从等待队列移除
                first.nextWaiter = null;
                /*while循环的transferForSignal才是真正的将头节点transfer(移动)到同步队列中的方法
                transferForSignal(first)返回false的话,再取反则为true,则继续走&&后面的,将头节点
                继续给first,然后继续准备循环移动。
                transferForSignal(first)返回true的话,就跳出循环了,代表移动成功了。
                */
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
}


    /**
     * Transfers a node from a condition queue onto sync queue.
     * 从等待队列移动到同步队列
     * Returns true if successful.
     * @param node the node
     * @return true if successfully transferred (else the node was
     * cancelled before signal)
     */
     //其实是AQS的方法
    final boolean transferForSignal(Node node) {
        /*
         * If cannot change waitStatus, the node has been cancelled.
         */
         //首先先把头节点的状态改为0,默认状态
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;

        /*
         * Splice onto queue and try to set waitStatus of predecessor to
         * indicate that thread is (probably) waiting. If cancelled or
         * attempt to set waitStatus fails, wake up to resync (in which
         * case the waitStatus can be transiently and harmlessly wrong).
         */
        //然后将节点加入到同步队列中,这边之前分析过了,就不再叙述
        Node p = enq(node);
        //此时p为node的前置节点
        int ws = p.waitStatus;
        /*
        ws > 0 什么意思,意思是取消了,显然一般不可能,false
        则继续compareAndSetWaitStatus(p, ws, Node.SIGNAL)将p的状态修改为SIGNAL true,取反false
        即ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL) false 则不执行
        这段判断我的理解是,防止出现意外状况,直接唤醒当前节点的线程,即前置节点取消了,或者更新前置节
        点的状态为SIGNAL失败了,其实就是对意外情况的补充操作。。。
        */
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        //最后并不影响返回值true
        return true;
    }

signal总结

总结如下:首先,执行condition.signal方法的前提是当前线程已经获取了lock,然后,取出等待队列的第一个节点(也是最早进入队列/等待时间最长的节点),通过CAS操作将节点重新移动到同步队列,此后就会回到await方法的循环中的LockSupport.park(this)方法中返回,从而才有机会使得调用await方法的线程成功退出await方法。然后线程就可以真正获取到锁资源进行业务的处理了。 到此condition的await(等待)/signal(唤醒)的闭环流程就结束了。至于signalAll方法有兴趣的朋友可自行研究下和signal的区别吧~~

(Condition.await/signal)VS(Object.wait/notify)

首先,我们根据以上Condition的分析,我模仿了之前讲synchronized同步锁的原理的图以及一位前辈文章中看到的(跟我画的synchronized的同步原理图好像,我们肯定是看了同一本书哈~),然后我自己再根据生产消费的场景又画了一幅图,如下:可以假设awaitThread是消费者,signalThread是生产者,然后消费者在等待生产者有产品出来,产品出来了就通知消费者从等待队列里出来进入同步队列竞争锁后进行相应的消费操作。 这幅图其实也代表了await、signal的协同闭环的流程(文字描述就不描述啦,其实图已经很清楚了~) image.png 再对比之前的synchronized的同步原理图: image.png 简直异曲同工之妙,相通的,不得不佩服Doug Lea大神啊!!!不同的是wait/notify貌似只有一个等待队列,而我们的Condition,可以通过new Condition,创建无数个等待队列,且Condition可供我们操作的API更丰富,其实也符合了文章开头讲的Lock和synchronized的区别。

Condition等待队列的一个典型应用类就是阻塞队列,比如ArrayBlockingQueue,这在我们介绍生产者/消费者的时候举例过,至于ArrayBlockingQueue的源码分析我们会在以后的篇幅中会介绍的。

终于结束了!!!在文章的最后,我顺便分享下,我画的所有结构/流程图的链接

文献参考

《Java并发编程的艺术》

猜你喜欢

转载自juejin.im/post/7112101778836946980
今日推荐