java锁ReentrantLock的源码分析

简单的函数介绍

声明锁对象,构造函数默认不传是创建非公平锁,传true是创建一个公平锁
ReentrantLock lock = new ReentrantLock(true);

重入锁,lock和unlock成对出现,
lock.lock();
lock.unlock();

尝试加锁,有返回值的加锁成功返回true,失败则
lock.tryLock();

锁阻塞期间,可以掉用线程的interrupt打断,然后用InterruptedException捕获来中断锁阻塞的状态,
lock.lockInterruptibly();
InterruptedException
myThread1.interrupt();

加锁过程猜测

了解过ReentrantLock里面有一个AQS(AbstractQueuedSynchronizer),抽象的队列同步的? 用它来实现的加锁解锁,这两天看了一点源码,做个笔记.
在看源码之前,首先猜测aqs应该就是一个队列,因为它有公平锁一说,所以必然队列是先进先出的,然后应该是当前对象获取到锁之后,后续进来的会去队列里排队等待,解锁之后,取出队列的第一个进行唤醒.

加锁流程源码分析

下面部分直接说结论了,不循序渐进了

首先看公平锁的实现,非公平锁相对还要简单一些.

在这里插入图片描述
我看下来,觉得公平锁的核心部分就是这个函数,实际上这里进行了3个比较重要的工作和1个不那么重要的工作,

  1. tryAcquire
  2. addWaiter
  3. acquireQueued
  4. selfInterrupt(不那么重要的部分)

tryAcquire

在这里插入图片描述
返回true则表示无需阻塞,直接运行即可,有两种可能,

  1. 当前状态无锁,并且没有执行中的队列
  2. 有锁,但持有者是自己.

返回false代表取锁失败,有几种可能

  1. 当前有锁,并且并且锁不是自己,直接退出,返回
  2. 当前无锁,但是有运行中的队列,公平锁的情况下,会跳出方法到外层的acquireQueued中继续,哪怕队列是空的. 非公平锁没有这一步判断,直接拿锁走人

简单说下代码流程

这个函数是获取锁的主要部分,而且后续也在其他流程里多次调用了这个函数,
state是当前锁状态, =0代表无锁,=1代表有锁
如果无锁状态下,会进入队列判断,也就是hasQueuedPredecessors

在这里插入图片描述
这个地方进行了多次的非判断,很干扰阅读的逻辑,
首先说明,AQS会单独存储队列中的头尾节点,tail和head.
首先头尾相等只有一种可能,就是两个都是null,所以用第一个判断来确定,如果都是null则没有正在跑的队列,
第二个是判断当前线程是不是head.next. 如果它不是则没有资格继续拿锁,直接返回false,如果是,才进入往下判断.

compareAndSetState() //更新状态,不说了,
setExclusiveOwnerThread() //设置当前拿锁线程,

下面这部分是判断重入锁的部分,如果当前线程就是拿到了锁的线程,则再次进入,并且State+=1

else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }

至此,tryAcquire部分结束,主要就是尝试加锁,true成功,false失败,

addWaiter

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

回到acquire,tryAcquire返回值为true,并且这里是"非"true的话,也就是说,返回true,就直接整个判断退出了(这里有点绕),如果是返回false,则进入下面一个函数,按照执行逻辑,先进入addWaiter.
在这里插入图片描述
AQS中的Q是个双向链表,其中的节点就是这个Node,结构为:waitStatus/prev/next/thread
刚才说了,会单独存储头尾节点,此时尝试取出尾结点,原则上新进来的线程需要加到队列的最尾部分,之前的尾步成为倒数第二,
如果尾结点==null,则会进入enq进行队列初始化

enq

在这里插入图片描述
死循环来保证,一定是有头尾节点后,才返回
逻辑稍微有点绕,简单的说

  1. 初始化头结点,并且传入的node是一个没有线程的,new出来的
  2. 尾=头
  3. cas,把尾节点设置成当前节点
  4. 头节点的next指定成当前节点

这有个比较怪的地方,
读代码,很容易误解成 t.next = node 是在把尾节点的next指定成当前节点,但实际上这个t是之前的head节点的引用,真正的尾节点已经在compareAndSetTail中更新成了当前节点.
为什么<<尾=头>>,而且下面还用了unsafe的方法,不能直接用两个引用分别做事情吗…

至此addWaiter结束,此时完成了对当前线程的节点封装,并且保证了队列当中有头尾节点.

acquireQueued

把刚才包装好的node加到队列里,
在这里插入图片描述
这里也是一个死循环,
先是取出当前节点的前一个,判断前一个是不是头结点,如果是,则再次调用tryAcquire来尝试获取锁资源,如果此时成功了,会更新节点信息后,直接放回,相当于拿到了锁,相当于一次自旋,
如果失败则进入第二个if,

 private static boolean shouldParkAfterFailedAcquire(Node pred, Node 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;
    }

上面代码由于源码中注释太多,没有用截图,直接把注释删掉贴了过来,
有一个需要注意的是,这里判断的主体都是以当前节点的前一个,如果前一个是SIGNAL(这理解不是很透彻,直接贴一段注释)

/** waitStatus value to indicate successor's thread needs unparking */
        static final int SIGNAL    = -1;

如果是SIGNAL则返回true,当前线程进入park
如果不是SIGNAL,则会进入到下面的else里,把上一个设置成SIGNAL,待死循环的下一次执行进来直接返回true.

解锁流程

逻辑猜测:
估计就是取当前队列的第一个,进行唤醒,应该比加锁过程简单的多
直接看码
在这里插入图片描述
上图是核心部分,“尝试解锁”,“尝试成功”,则取到head,进行unpark唤醒

在这里插入图片描述

  1. 判断当前线程是不是持有锁的线程,如果不是直接抛异常了
  2. 设置aqs的state为0,设置持有锁的线程为null,相当于释放了锁资源

在这里插入图片描述
取到head,如果head的waitStatus>0则不需要唤醒任何,<0则设置成0,并且走唤醒逻辑
取到头的next节点,判断非空,或者等待状态>0,正常情况下它应该是-1的,这种是排除队列中有其他的异常情况,注释中写道<<要解锁的通常是h的下一个节点,但如果出现了明显的null,或状态取消,则从尾部开始向前便利.>>
一个for循环,不断从后向前便利,改变目标节点的值,最终取到的目标节点就是,在队列里面第一个不为空且状态是对的,
拿到节点,进行unpark

tryLock,和lockInterruptibly

在这里插入图片描述
tryLock,直接调用了非公平锁的,请求锁方法tryAcquire,并且直接把tryAcquire的返回值返回了.相当于没有取到锁的话,没有后续的排队环节,直接返回false了.

在这里插入图片描述
lockInterruptibly,等待锁的过程中,可以调用线程的Interrupt进行打断,然后用一个异常捕捉到,然后让线程继续做别的事情.核心在于这个地方直接抛出了异常,

总结

一会会画一个流程图,走一遍流程,
代码里有两个我不太喜欢的地方
1:有太多的双非判断,不好理解,
2:执行逻辑和判断逻辑是结合在一起的,我一般会尽量的把执行和判断分开,哪怕代码执行效率会略微下降一点,尽量避免在if括号里进行大量的工作执行.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章