java多线程解说【伍】_锁实现:ReentrantLock的实现

上篇文章: java多线程解说【肆】_锁实现:wait()/notify()



通过上文我们得知,使用Object的wait()/notify()在释放锁**其他线程的时候,不能指定**只能随机**,而我们实际开发场景中可能需要去指定**某个阻塞的线程,那么怎么办呢?别急,uc包(java.util.concurrent)下为我们提供了灵另外一种锁实现,这就是Lock/Condition模式。


JUC包


首先看下uc包的类关系图:


java多线程解说【伍】_锁实现:ReentrantLock的实现


后面我们要说的Lock和Condition都是接口,而作为java语言中显示锁实现的担当ReentrantLock则不得不着重说一下。顾名思义,它是一个可重入锁,也就是说线程可以多次进出。同时它还支持一个重要的功能,就是公平锁,可通过其构造函数ReentrantLock(boolean isFair)来声明,当参数传入为true时则该锁为一个公平锁,默认为false非公平锁。那么什么是公平锁和非公平锁?公平锁是指多个线程同时尝试获取同一把锁时,获取锁的顺序严格按照线程达到的顺序;而非公平锁则允许线程“插队”。通过定义我们可以推测出,在锁的内部应该是维护了一个等待锁的线程队列,那么这个队列是怎么实现的呢?


AQS


这就要提到AbstractQueuedSynchronizer了,人称AQS。它也是juc包中不可或缺的一员,为uc包中锁(ReentrantLock、LockSupport)和同步器(Semaphore、CountDownLatch)提供了同步实现框架。它是一个抽象类,内部主要维护了如下几个对象来实现功能:


state:资源状态。当资源可用时state=0,资源被上锁时state+1,释放锁时state=1,注意,这里是锁是支持重入的,也就是说state可以大于1,但是不管加了多少次锁,想让出资源必须报保证state=0,也就是还要释放多少次锁;


exclusiveOwnerThread:持有对象锁的线程。


node:"CLH"队列中的一个节点,该队列是来记录排队等待的线程基于先进先出(FIFO)规则的双向链表实现。至于为什么叫CLH,因为这是三个人名字的首字母缩写...一个节点主要包含如下组成:

  prev:前序节点

  next:后序节点

  thread:当前节点对应的线程

  waitStatus:当前节点的等待状态,初始值为0,枚举有:1.被中断;-1.阻塞后续节点;-2.条件等待(Condition功能用)

  nextWaiter:下一个等待节点(Condition功能用)


head"CLH"队列的头结点。


tail"CLH"队列的尾结点。


这个队列的结构大略如下图所示:


java多线程解说【伍】_锁实现:ReentrantLock的实现

那么QAS到底是怎么去支持ReentrantLock的实现,以及公平锁和非公平锁的呢?



ReentrantLock的实现


ReentrantLock实现类实现了Lock接口,其中最重要的两个方法就是lock()和unLock(),下面尝试使用伪代码来分析一下它们是如何基于AQS来实现的(下流程以非公平锁为例)。


1.当第一个线程来调用lock()方法时,首先去判断当前AQS中资源状态是否可用(state==0),发现可用,则将state+1,并把自己的引用写到持有锁线程对象(exclusiveOwnerThread)。


2.第二个线程来调用lock()方法,还是先判断当前AQS中资源状态是否可用(state==0),发现不可用,则进入加锁处理流程:

  2.1 再获取一遍资源状态是否可用(state==0),如果可用,则将state+1,并把自己的引用写到持有锁线程对象(exclusiveOwnerThread),直接返回;

  2.2 判断当前持有锁线程对象(exclusiveOwnerThread)是否就是自己,如果是,则将state+1,直接返回;

  2.3 前两步都不满足,则进入线程加入CLH队列逻辑:

    2.3.1 先判断尾节点(tail)是否存在,如果存在则证明CLH队列已经初始化过,直接把自己接到尾节点后面即可;

    2.3.2 如果尾节点(tail)为空,则说明CLH队列没有初始化,进入CLH队列初始化逻辑(自旋模型):

      2.3.2.1 第一次循环,尾节点(tail)为空,则新建一个空节点node,将头结点(head)和尾节点(tail)都指向这个空节点;

      2.3.2.2 第二次循环,尾节点(tail)不为空,把自己接到尾节点后面,跳出自旋循环,此时CLH队列初始化已完成;

    2.3.3 进入尝试获取锁逻辑(自旋模型):

      2.3.3.1 先判断前序节点是否为头结点(head),如果是则尝试获取锁,获取锁即2.1和2.2的逻辑,如果获取成功则将自己设为头结点(head),跳出自旋循环;

      2.3.3.2 尝试获取锁失败,进入判断是否开始竞争锁逻辑(自旋模型):

                         2.3.3.2.1 如果前序节点等待状态(waitStatus)前序节点等待状态waitStatus>0(前序节点的线程被中断),则删除前序节点,继续循环;

                         2.3.3.2.2 如果前序节点等待状态waitStatus=-1(阻塞后续节点),则跳出循环,当前节点阻塞;

                         2.3.3.2.3 不属于上述两种情况,则将前序节点的waitStatus置-1(阻塞后续节点),继续循环;


(走到这里时,第二个线程处于阻塞状态,如果再有第三个线程请求锁,同上述从2开始的逻辑也会处于阻塞状态,并在队列上排在第二个线程之后)


3.第一个线程调用unLock()方法时,则进入释放锁的逻辑:

  3.1 先计算当前的AQS中资源状态state-1;

  3.2 判断一把持有锁线程对象(exclusiveOwnerThread)是否为自己,如果不是则抛出异常(感觉这一步可以拿到前面去做);

  3.3 更新AQS中资源状态state;

  3.4 判断state-1是否为0,如果是则说明当前锁已经释放,将持有锁线程对象(exclusiveOwnerThread)置空,之后判断前序节点是头结点(head)且其等待状态(waitStatue)不为0,则进入**后续节点逻辑:

    3.4.1 如果头节点(head)的等待状态(waitStatus)<0,则将其等待状态(waitStatue)置0;    

    3.4.2 获取头结点(head)的后续节点,判断其是否为空且等待状态(waitStatus)大于0(说明被中断):

      3.4.2.1 如果是,则从尾节点开始向前遍历,直到找到一个等待状态不大于0的节点,将其唤醒(unPark);

      3.4.2.2 如果不是,则说明头结点(head)的后续节点正在等待唤醒,将其唤醒。


至此,ReentrantLock的lock()和unLock()方法的底层实现伪代码就说完了,第一次看的时候确实有些绕且不好理解,建议结合JDK源码及这篇文章一起看,再回来看上面的伪代码时或许就会有新的感悟。


公平锁/非公平锁的区别


如上文所说,ReentrantLock是支持公平锁和非公平锁的,通过创建实例的时候传入isFair参数决定。那么在底层ReentrantLock是如何实现的呢?我们通过看源码得知,ReentrantLock内部维护了一个Sync对象,该对象继承于AbstractQueuedSynchronizer,也就可以调用AbstractQueuedSynchronizer所有的方法。其实上面篇章中伪代码的描述,很大一部分都是针对AbstractQueuedSynchronizer中的方法。而Sync也是一个抽象类,继承于它的类有两个,分别为NonfairSync何FairSync,看名字也得知一个是非公平同步类一个是公平同步类。ReentrantLock就是通过构造方法传入的isFair参数,决定创建哪个对象的实例。


那么还有个问题,既然说公平锁和非公平锁,那么这个公平的标准到底是什么?其实主要就在于上面伪代码中的第一步,也就是当一个线程尝试去获取所的时候:


1.当第一个线程来调用lock()方法时,首先去判断当前AQS中资源状态是否可用(state==0),发现可用,则将state+1,并把自己的引用写到持有锁线程对象(exclusiveOwnerThread)。


其实这个描述就是非公平锁的实现,它不会去判断CLH队列中是否还有其他线程在排队等候,而是当判断到当前资源状态是可用的就直接去尝试获取锁了。如果竞争到锁,则直接执行;如果没有竞争到,则加入到CLH队列的末尾排队等待。一旦进入到CLH队列后,就只能排队等待唤醒了。


而公平锁的实现则是,当一个线程尝试去调用lock()方法时,会先判断是否满足下列条件之一:


1.头结点(head)为空;

2.头结点的后续节点不为空且为自己;

3.下一个将执行的节点关联的线程正是自己的线程;


如果不满足,则会加入到CLH队列的末尾排队等待。


以上就是ReentrantLock的锁实现。但是ReentrantLock还支持了条件机制(Condition),可针对不同业务场景指定不同的锁条件,那么Condition是如何实现和使用的呢,请看下文:


java多线程解说【陆】_锁实现:Condition的实现