ReentrantLock 加锁解锁,部分AQS源码解析(上)

 ReentrantLock 锁 加锁和解锁

  我们从常用的lock方法,为入口,来阅读一下lock方法,实际上在源码中执行的过程。

  

   进入方法中,我们看到调用的是sync.lock()方法

  

  首先,我们来看看sync这个是一个什么类

   

   从这里我们可以看出他是一个抽象类,而且是继承了AQS类,我们进入  AbstractQueuedSynchronizer()这个父类,会发现,里面维护了一个双向链表数据结构,这里我们先了解他是一个双向链表的结构就可以了,为什么确定他是一个双向链表的结构,因为仔细看,node节点中的部分属性

  

  prev代表上一个node  next代表下一个node  Thread代表本node存储的是一个Thread线程,从这里我们就可以看出,AQS维护的是一个 双向链表,链表内存储的对象是Thread对象,下面是AQS中存储线程使用的结构,可以看成是保存线程的队列。

  

  回归正题 ,我们回到 sync.lock(); 处,点击lock 发现到了一个抽象方法中,Sync类中并没有给我们提供默认的实现,而是需要实现子类自己来实现,这中模式,我们叫做模版方法设计模式

  

   同时按住ctrl+alt,鼠标点击lock()方法,会给我们提示出两个实现了lock方法的子类方法如下图:

  

  FairSync 和 NonFairSync 看名字就能看出来, 对应的锁分别是 基于 公平锁的实现和非公平的实现,本次,我们跟着非公平的实现接着往里面走。

  1. 在代码中调用了lock()方法,尝试获取锁,此时没有线程持有这把锁,直接获取锁成功时代码的调用过程。

  非公平的Sync类实现了lock类,在一个线程进入lock时,会调用AQS的 compareAndSetState(0,1)方法,而这方法又调用了unsafe的方法

  

  

  这个方法是基于C/C++实现的native方法,这边我们看不到源码,但是这个实际上是使用cas自旋的方式,判断是否能够将预期的0 改成 1,这边可以看成是,state = 0,尝试把0改成1 ,如果修改成功了就进入 setExclusiveOwnerThread方法,这个方法只做一件事情,就是设置持有锁的线程是哪个线程。

  

 

   2.获取锁失败了,发现已经有线程持有了这把锁

  调用 esle 里acquire(1) 方法 这里传递的1是一个固定参数,是用来给state赋值或者重入的。

  

   跟进方法之后发现在一个判断语句中,会调用tryAcquire(arg) 再次尝试获取这把锁,在这个方法中调用了nonfairTryAcquire()方法,我们看一下方法中获取锁的过程,

  

   我们将这个过程做一下流程的分解

  1.获取当前尝试获取锁的线程。

  2.调用getState方法,获取当前的state的值。

  3.判断state是否等于0。

  4.如果等于0 ,尝试将state改为1,如果成功,就代表加锁线程,设置当前线程为持有锁的线程,并且返回true。

  5.如果不等于,但是当前线程是持有锁的线程,代表重入,将state+1并重新复制给state,这一段说明 AQS是可重入锁。返回true

  6. 如果以上步骤都失败了,那么返回false。

  上面我们将尝试获取锁这个方法做了简单的分解,代表一个线程,在调用lock方法时,底层代码是怎么来获取锁的。

  假设,我们在这一段获取锁失败了,返回了false,我们继续回到 acquire(1) 方法中。

  因为第一个判断是当前线程去尝试获取锁,如果获取成功了,返回true 则 !true 条件不成立,就不会进去之后的判断了,但是我们当前的这个线程没有获取到锁,返回false  !false 第一个条件成立,我们接着看第二个判断条件。

  


   我们先看addWaiter()方法,这个方法是将当前的线程,创建成AQS维护的一个双向链表

   

   但是这个中间有对即将加入链表尾部的node做一些判断,下面我们一起来分析一下

  1.创建一个新的node节点,查看一下创建node几点的构造方法

   

   第一个参数,将节点内的线程赋值,这个没啥问题,第二个参数是addWaiter传递过来的一个常量值,我们看一下这个常量值的解释

  

  这个地方是表示,创建一个节点,这个节点是独占模式,也就是独享这把锁,不会分享出去。

  2.获取到链表中标记出来的尾部节点。

  3.使用cas尝试将当前新建的节点变更成尾部节点,如果成功了,就将旧的尾部节点的next引用指向自己,并返回这个node。

  4.如果失败了尾部节点是空的,进入end(node)方法,我们看一下end(node方法)

  

   end方法通过死循环,来判断,如果最后一个节点为空节点,那么那么将一个空的node节点设置为头部节点,初始化这个链表结构,将头部和尾部都指向这个新节点。

  如果最后一个节点不是空的节点了,因为可能有线程在这个时候已经对链表进行过初始化了,那么将当前的节点的上一个节点设置为尾部节点,然后使用cas操作来自旋,直到修改完成后,才会返回最后一个节点出去。总结一下就是:

  我进入一个死循环,在这个循环中,我预计将我的节点,改成尾部节点,如果在我修改的时候对数据的时候,已经有新的线程,将尾部节点修改了,那么我继续拿最新的尾部节点来修改,直到达成我的目的,将我要设置的node节点,

变成了尾部节点,我才退出死循环。这一步主要是为了保证,我的节点,一直能成功的在多线程的情况下,进入并且保证是最后一个节点。

  在看完了addWaiter()方法后,我们接着来看 acquireQueued()方法,这个方法要求的参数是 我们通过addWaiter() 返回的最后一个节点,和int 类型的值 1

  

   方法里呢,也是一个cas操作,但是这次是判断头部线程的状态了,接下来我们分析一下代码里的流程:

  1.先初始化两个个boolean的值,用来标记。

  2.进入死循环, 获取获取当前节点的上一个节点。

  3.判断上一个节点是不是等于头一个节点,如果等于头一个节点,再次去尝试获取锁。

  4.如果获取成功,将本节点设置为头部节点,把上一个节点对本节点的引用置空,可以看到注释是为了帮助垃圾回收,然后将第一个标记设置为false标记,然后返回第二个标记,如果获取成功了,那么在这一步就返回了false,那么

acquire()方法的判断也就不成立, 所以就不用在进行下去了,因为当前线程已经获取到锁了。

  5.如果没有获取到锁,就进入shouldParkAfterFailedAcquire()方法,这个方法主要是判断当前节点和上一个节点的状态,如果上一个节点等于阻塞状态,那么返回true。

    如果上一个节点的状态>0,那么代表上一个节点已经被通知取消继续执行,那么跳过上一个节点,将上一个节点赋值为上一个节点的上一个节点,然后再赋值 当前节点的上一个节点为上一个节点 ,如果新循环之后,上一个节点还是取消状态,那么就一直循环,直到上一个节点的状态正常,并把上一个节点的的next指向当前节点  node.prev = pred = pred.prev; 这段的含义就相当于  int x,y,z=0 x=y=z 就相当于先把 y = z ,然后 x = y(这段我一开始还没有看懂...)返回false;

    如果上一个节点的条件 <0 那么尝试将状态修改为-1 不管成功不成功返回false;

  

  6.在判断完成节点状态之后,我们锁住当前的对象来检查检查当前线程是否被停止了,如果停止了,将第二个标记改成true;

  7.最后,代码在退出死循环的时候,有一个finally 代码块,这里判断如果第一个标记是true的话,就取消获取锁并将当前节点从链表中删除,代码中的主要逻辑是尝试将当前节点的上一个节点设置为尾部,如果设置成功了,但是发现有新的线程在当前节点后面了,就移动next节点,只想 上一个节点,最后将当前节点的下一个节点指向自身,帮助GC。

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

  最后,在执行完成 acquireQueued()方法后,如果返回的是true,就代表当前的线程已经被取消了,所以直接返回就可以了。

  总结: 自己跟着断点进入了ReentrantLock的源码,然后进入了AQS 的源码,跟着断点来学习源码是怎么使用cas,来对一个对象 不是用 synchronize 来加锁,可能是最近学习的东西太多了,没有进行归纳总结,所以容易忘记,之后会对学习过的东西归纳总结,避免学习了之后又忘记=白学。这一篇是跟进lock.lock()方法,下一篇是跟进 loca.unlock()方法。

猜你喜欢

转载自www.cnblogs.com/Yye0118/p/13373395.html