JUC——通过ReentrantLock的加锁过程说AQS

之前写了JUC——volatile关键字的使用以及底层原理浅析JUC——CAS技术以及底层原理,也是为了后面说JUC里面的知识做铺垫的,本文我们再来说下AQS

通过ReentrantLock的构造函数可知,默认为非公平锁

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

 public ReentrantLock(boolean fair) {
      sync = fair ? new FairSync() : new NonfairSync();
 }

公平锁: 

sync是 AbstractQueuedSynchronizer (AQS)的子类

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

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

        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            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;
            }
            return false;
        }
    }

非公平锁:

    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

除了不需要判断是否需要排队之外,其他逻辑与公平锁一致

final boolean   nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {//直接cas获取锁
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            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;
        }

 tryAcquire方法公平锁与非公平锁又各自重写了

public final void acquire(int arg) {
    //tryAcquire(arg)尝试加锁,如果加锁失败则会调用acquireQueued方法加入队列去排队,如果加锁成功则不会调用
    //acquireQueued方法下文会有解释
    //加入队列之后线程会立马park,等到解锁之后会被unpark,醒来之后判断自己是否被打断了;被打断下次分析
    //为什么需要执行这个方法?下文解释
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

 

由此可以看出公平锁合非公平锁的区别如下:

公平锁的上锁是必须判断自己是不是需要排队;而非公平锁是直接进行CAS修改state看能不能加锁成功;如果加锁不成功则乖乖排队(调用acquire);所以不管公平还是不公平;只要进到了AQS队列当中那么他就会排队;一朝排队;永远排队记住这点.

 

tryAcquire方法源码分析 

protected final boolean tryAcquire(int acquires) {
    //获取当前线程
    final Thread current = Thread.currentThread();
    //获取lock对象的上锁状态,如果锁是自由状态则=0,如果被上锁则为1,大于1表示重入
    int c = getState();
    if (c == 0) {//没人占用锁--->我要去上锁----1、锁是自由状态
        //hasQueuedPredecessors,判断自己是否需要排队这个方法比较复杂,
        //下面我会单独介绍,如果不需要排队则进行cas尝试加锁,如果加锁成功则把当前线程设置为拥有锁的线程
        //继而返回true
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            //设置当前线程为拥有锁的线程,方便后面判断是不是重入(只需把这个线程拿出来判断是否当前线程即可判断重入)    
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //如果C不等于0,而且当前线程不等于拥有锁的线程则不会进else if 直接返回false,加锁失败
    //如果C不等于0,但是当前线程等于拥有锁的线程则表示这是一次重入,那么直接把状态+1表示重入次数+1
    //这里也侧面说明了reentrantlock是可以重入的,因为如果是重入也返回true,也能lock成功
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }

protected final Thread getExclusiveOwnerThread() {
        return exclusiveOwnerThread;
    }

重点: 

 public final boolean hasQueuedPredecessors() {
        Node t = tail; //尾部
        Node h = head; //头部
        Node s;       //头结点的下一个节点
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

这里有个十分重要的点要交代:node0要么是虚拟出来的节点,要么是持有锁的节点;什么时候是虚拟的呢?什么时候是持有锁的节点呢?下面分析中有答案

结果一:不需要排队,也即hasQueuedPredecessors  返回false

情况一:h != t 为false,则短路与判断就会直接返回false

说明:当头节点和尾节点相等时,会返回false。
1、头节点和尾节点都为null,表示队列都还是空的,甚至都没完成初始化,那么自然返回fasle,无需排队。
2、头节点和尾节点不为null但是相等,说明头节点和尾节点都指向一个元素,表示队列中只有一个节点(其实可看做队列被初始化了,但是FIFO中只有node0),这时候自然无需排队,因为队列中的第一个节点是不参与排队的,它持有着同步状态,那么第二个进来的节点(node1)就无需排队,因为它的前一个节点就是头节点,所以第二个进来的节点就是第一个能正常获取同步状态的节点,第三个节点(node2)才需要排队,等待第二个节点(node1)释放同步状态。 

情况二:h != t 返回true,(s = h.next) == null返回false以及s.thread !=Thread.currentThread()返回false
说明:h != t 返回true,表示队列中至少有两个不同节点存在,如只存在node0和node1,(s = h.next) == null返回false 说明头结点是有后继节点的;

s.thread != Thread.currentThread()返回fasle,说明s.thread == Thread.currentThread(),也即 已经轮到这个线程相关的节点(node1)去尝试获取同步状态了,自然无需排队,直接返回fasle
 

不需要排队的总结:队列没有初始化,可直接去竞争锁;

                                队列被初始化了,且刚好轮到自己去竞争锁了。

         

结果二:需要排队,也即hasQueuedPredecessors返回true

情况一: h != t返回true,(s = h.next) == null返回true

说明:h != t返回true表示队列中至少有两个不同节点存在(如存在node0、node1)。

(s = h.next) == null返回true,说明头节点之后是没有后继节点的,这情况可能发生在如下情景:有另一个线程已经执行到初始化队列的操作了,介于compareAndSetHead(new Node())与tail = head之间,如下

这时候头节点不为null,而尾节点tail还没有被赋值,所以值为null,所以会满足h != t结果为true的判断,以及头节点的后继节点还是为null的判断,这时候可以直接返回true,表示要排队了,因为在当前线程还在做尝试获取同步状态的操作时,已经有另一个线程准备入队了,当前线程慢人一步,自然就得去排队。

情况二:h != t返回true,(s = h.next) == null返回false,s.thread !=Thread.currentThread()返回true。

说明:h != t返回true表示队列中至少有两个不同节点存在(如存在node0、node1)。
(s = h.next) == null返回false表示首节点是有后继节点的。
s.thread != Thread.currentThread()返回true表示后继节点的相关线程不是当前线程,所以首节点虽然有后继节点,但是后继节点相关的线程却不是当前线程,那当前线程自然得老老实实的去排队。

 

需要排队的场景总结:并发状态下竞争锁的时候失败;

                                   正常情况下的排队,还未轮到自己尝试获取锁。

 

到此,我们已经分析完了tryAcquire方法,为了方便,再贴一次该方法:

public final void acquire(int arg) {
    //tryAcquire(arg)尝试加锁,如果加锁失败则会调用acquireQueued方法加入队列去排队,如果加锁成功则不会调用
    //acquireQueued方法下文会有解释
    //加入队列之后线程会立马park,等到解锁之后会被unpark,醒来之后判断自己是否被打断了
    //为什么需要执行这个方法?下次解释
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

static final class Node {
        static final Node SHARED = new Node();
        static final Node EXCLUSIVE = null;
        static final int CANCELLED =  1;
        static final int SIGNAL    = -1;
        static final int CONDITION = -2;
        static final int PROPAGATE = -3;
        volatile int waitStatus;
        volatile Node prev;
        volatile Node next;
        volatile Thread thread;
        Node nextWaiter;

        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }
    }

如果加锁失败,我们来看下是如何加入队列的

 

acquireQueued(addWaiter(Node.exclusive),arg))方法解析:

如果代码能执行到这里说线程需要排队,这里有两种情况:
1、队列并没有初始化;
2、队列是一定被初始化了的。

private Node addWaiter(Node mode) {
    //由于AQS队列当中的元素类型为Node,故而需要把当前线程tc封装成为一个Node对象,下文我们叫做nc
    Node node = new Node(Thread.currentThread(), mode);
    //tail为队尾,默认为null,赋值给pred 
    Node pred = tail;
    //判断pred是否为空,其实就是判断队尾是否有节点,其实只要队列被初始化了队尾肯定不为空,
    //假设队列里面只有一个元素,那么队尾和队首都是这个元素
    //换言之就是判断队列有没有初始化
    //上面我们说过代码执行到这里有两种情况,1、队列没有初始化和2、队列已经初始化了
    //pred不等于空表示第二种情况,队列被初始化了,如果是第二种情况那比较简单
   //直接把当前线程封装的nc的上一个节点设置成为pred即原来的队尾
   //继而把pred的下一个节点设置为当nc,这个nc自己成为队尾了
    if (pred != null) {
        //直接把当前线程封装的nc的上一个节点设置成为pred即原来的队尾,对应 10行的注释
        node.prev = pred;
        //这里需要cas,因为防止多个线程加锁,确保nc入队的时候是原子操作
        if (compareAndSetTail(pred, node)) {
            //继而把pred的下一个节点设置为当前nc,这个nc自己成为队尾了 对应第11行注释
            pred.next = node;
            //然后把nc返回出去,方法结束
            return node;
        }
    }
    //如果上面的if不成立就会执行到这里,表示第一种情况队列并没有初始化---下面解析这个方法
    enq(node);
    //返回nc
    return node;
}

private transient volatile Node tail;

总结:addWaiter就是将当前线程加入队列,一共有两种情况:

1.队列未被初始化,则不走if逻辑,走enq方法,下面分析;

2.队列已被初始化,走if逻辑,将对尾赋给当前节点(node)的prev属性(Node pred = tail; node.prev = pred; ),当前节点入队(pred.next= node)

我们再来分析enq(node)方法:

private Node enq(final Node node) {//这里的node就是当前线程封装的node也就是nc
    //死循环
    for (;;) {
        //队尾复制给t,上面已经说过队列没有初始化,且队尾默认为null
        //故而第一次循环t==null(因为是死循环,因此强调第一次,后面可能还有第二次、第三次,每次t的情况肯定不同)
        Node t = tail;
        //第一次循环成了成立
        if (t == null) { // Must initialize
            //new Node就是实例化一个Node对象下文我们称为nn,
            //调用无参构造方法实例化出来的Node里面三个属性都为null,可以关联Node类的结构,
            //compareAndSetHead入队操作;把这个nn设置成为队列当中的头部,cas防止多线程、确保原子操作;
            //记住这个时候队列当中只有一个,即nn,且属性都为null
            if (compareAndSetHead(new Node()))//虚拟的头节点
                //这个时候AQS队列当中只有一个元素,即头部=nn,所以为了确保队列的完整,设置头部等于尾部,即nn即是头也是尾
                //然后第一次循环结束;接着执行第二次循环,第二次循环代码我写在了下面,接着往下看就行
                tail = head;
                //此步骤结束后,对列结构请查看图1
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

//node主要结构,详细的上面有贴出来
public class Node{
    volatile Node prev;
    volatile Node next;
    volatile Thread thread;
}

//为了方便 第二次循环我再贴一次代码来对第二遍循环解释
private Node enq(final Node node) {//这里的node就是当前线程封装的node也就是nc
    //死循环
    for (;;) {
        //对尾复制给t,由于第二次循环,故而tail==nn,即new出来的那个node
        Node t = tail;
        //第二次循环不成立
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            //不成立故而进入else
            //首先把nc,当前线程所代表的node的上一个节点改变为nn,因为这个时候nc需要入队,入队的时候需要把关系维护好
            //所谓的维护关系就是形成链表,nc的上一个节点只能为nn,这个很好理解
            node.prev = t;
            //入队操作--把nc设置为对尾,对首是nn,
            if (compareAndSetTail(t, node)) {
                //上面我们说了为了维护关系把nc的上一个节点设置为nn
                //这里同样为了维护关系,把nn的下一个节点设置为nc
                t.next = node;
                //然后返回t,即nn,死循环结束,enq(node);方法返回
                //这个返回其实就是为了终止循环,返回出去的t,没有意义
                //此步骤结束后队列结构如图二
                return t;
            }
        }
    }
}

总结:addWaiter方法就是让nc入队-并且维护队列的链表关系,但是由于情况复杂做了不同处理 -------------------主要针对队列是否有初始化,没有初始化则new一个新的Node nn作为对首,nn里面的线程为null -------------------接下来分析acquireQueued方法

到这,当前被阻塞的线程nc已入队,我们再来看acquireQueued方法:

final boolean acquireQueued(final Node node, int arg) {//这里的node 就是当前线程封装的那个node 下文叫做nc
    //记住标志很重要
    boolean failed = true;
    try {
        //同样是一个标志,是否打断
        boolean interrupted = false;
        //死循环
        for (;;) {
            //获取nc的上一个节点,有两种情况;1、上一个节点为头部;2上一个节点不为头部
            final Node p = node.predecessor();
            //如果nc的上一个节点为头部,则表示nc为队列当中的第二个元素,为队列当中的第一个排队的Node;
            //这里的第一和第二不冲突;我上文有解释;
            //如果nc为队列当中的第二个元素,即为第一个排队的,则调用tryAcquire去尝试加锁---关于tryAcquire看上面的分析
            //只有nc为第二个元素;第一个排队的情况下才会尝试加锁,其他情况直接去park了,
            //因为第一个排队的执行到这里的时候需要看看持有锁的线程有没有释放锁,释放了就轮到我了,就不park了
            //有人会疑惑说开始调用tryAcquire加锁失败了(需要排队),这里为什么还要进行tryAcquire不是重复了吗?
            //其实不然,因为第一次tryAcquire判断是否需要排队,如果需要排队,那么我就入队;
            //当我入队之后我发觉前面那个人就是第一个,持有锁的那个,那么我不死心,再次问问前面那个人搞完没有
            //如果他搞完了,我就不park,接着他搞我自己的事;如果他没有搞完,那么我则在队列当中去park,等待别人叫我
            //但是如果我去排队,发觉前面那个人在睡觉,前面的前面的那个人都在睡觉,那么我也睡觉把---------------好好理解一下
            if (p == head && tryAcquire(arg)) {
                //能够执行到这里表示我来加锁的时候,锁被持有了,我去排队,进到队列当中的时候发觉我前面那个人没有park,
                //前面那个人就是当前持有锁的那个人,那么我问问他搞完没有
                //能够进到这个里面就表示前面那个人搞完了;所以这里能执行到的几率比较小;但是在高并发的世界中这种情况真的需要考虑
                //如果我前面那个人搞完了,我nc得到锁了,那么前面那个人直接出队列,我自己则是队首;这行代码就是设置自己为队首
                setHead(node);
                //这里的P代表的就是刚刚搞完事的那个人,由于他的事情搞完了,要出队;怎么出队?把链表关系删除
                p.next = null; // help GC
                //设置标识---记住加锁成功的时候为false
                failed = false;
                //返回false;为什么返回false?比较复杂和加锁无关
                return interrupted;
            }
            //进到这里分为两种情况
            //1、nc的上一个节点不是头部,说白了,就是我去排队了,但是我上一个人不是队列第一个
            //2、第二种情况,我去排队了,发觉上一个节点是第一个,但是他还在搞事没有释放锁
            //不管哪种情况这个时候我都需要park,park之前我需要把上一个节点的状态改成park状态
            //这里比较难以理解为什么我需要去改变上一个节点的park状态呢?每个node都有一个状态,默认为0,表示无状态
            //-1表示在park;当时不能自己把自己改成-1状态?为什么呢?因为你得确定你自己park了才是能改为-1;
            //不然你自己改成自己为-1;但是改完之后你没有park那不就骗人?
            //你对外宣布自己是单身状态,但是实际和某某私下约会;这有点坑人
            //所以只能先park;在改状态;但是问题你自己都park了;完全释放CPU资源了,故而没有办法执行任何代码了,
            //所以只能别人来改;故而可以看到每次都是自己的后一个节点把自己改成-1状态
            if (shouldParkAfterFailedAcquire(p, node) &&
                //改上一个节点的状态成功之后;自己park;到此加锁过程说完了
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
//如果竞争锁失败则走shouldParkAfterFailedAcquire,会走两次
//第一次进入:设置上一个节点的waitStatus为-1,即SIGNAL
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;//默认值为0
        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 {
            //将头节点的waitStatus设为-1
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

//第二次进入:返回true,去执行后面的代码->park自己,老老实实排队
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;//第一次循环已设为了-1
        if (ws == Node.SIGNAL) //结果为true,并返回true
            return true;
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            //将头节点的waitStatus设为-1
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

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

至此,我们通过Reentrantlock的加锁过程讲解了AQS的思想:CAS+自旋+状态量(volatile state)+CLH队列,思理简单,实现起来却是很复杂

总结下:

AQS的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系。
AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node),来实现锁的分配。

用大白话来说,AQS就是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。

注意:AQS是自旋锁:在等待唤醒的时候,经常会使用自旋(while(!cas()))的方式,不停地尝试获取锁,直到被其他线程获取成功

AQS 定义了两种资源共享方式:
1.Exclusive:独占,只有一个线程能执行,如ReentrantLock
2.Share:共享,多个线程可以同时执行,如Semaphore、CountDownLatch、ReadWriteLock,CyclicBarrier  后续有专门的博客继续探讨

 

加锁过程总结:
如果是第一个线程tf,那么和CLH队列无关,线程直接持有锁,并且也不会初始化队列,如果接下来的线程都是交替执行,那么永远和AQS队列无关,都是直接线程持有锁,如果发生了竞争,比如tf持有锁的过程中t2来lock,那么这个时候就会初始化AQS的CLH,初始化CLH的时候会在队列的头部虚拟一个Thread为null的Node,因为队列当中的head永远是持有锁的那个node(除了第一次会虚拟一个,其他时候都是持有锁的那个线程锁封装的node,只不过指向的线程为null),现在第一次的时候持有锁的是tf而tf不在队列当中所以虚拟了一个node节点,队列当中的除了head之外的所有的node都在park(不消耗资源的阻塞),当tf释放锁之后unpark某个(基本是队列当中的第二个,为什么是第二个呢?前面说过head永远是持有锁的那个node,当有时候也不会是第二个,比如第二个被cancel之后,至于为什么会被cancel,不在我们讨论范围之内,cancel的条件很苛刻,基本不会发生)node之后,node被唤醒,假设node是t2,那么这个时候会首先把t2变成head(sethead),在sethead方法里面会把t2代表的node设置为head,并且把node的Thread设置为null,为什么需要设置null?其实原因很简单,现在t2已经拿到锁了,node就不要排队了,那么node对Thread的引用就没有意义了。所以队列的head里面的Thread永远为null。

这里也推荐一下讲解AQS的视屏,可以结合着看  https://www.bilibili.com/video/BV19J411Q7R5?p=13

 

本文参考:https://blog.csdn.net/weixin_38106322/article/details/107154961

                 https://blog.csdn.net/java_lyvee/article/details/98966684

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值