摘要:因为在Concurrent包中的锁都是“可重入锁”,所以一般都命名为ReentrantX,因为所有的锁。“可重入锁”是指当一个线程调用 object.lock拿到锁,进入互斥区后,再次调用object.lock,仍然可以拿到该锁。很显然,通常的锁都要设计成可重
锁的可重入性
因为在Concurrent包中的锁都是“可重入锁”,所以一般都命名为ReentrantX,因为所有的锁。“可重入锁”是指当一个线程调用 object.lock拿到锁,进入互斥区后,再次调用object.lock,仍然可以拿到该锁。很显然,通常的锁都要设计成可重入的,否则就会发生死锁。
前面讲的synchronized关键字,同样是可重入锁。考虑下面的典型场景:在一个synchronized函数f1里面调用另外一个synchronized函数f2。如果synchronized关键字不可重入,那么在f2处就会发生阻塞,这显然不可行。
类的继承层次
在正式介绍锁的实现原理之前,先看一下 Concurrent 包中的与互斥锁(ReentrantLock)相关类之间的继承层次,如图3-1所示。
在图3-1中,I表示界面(Interface),A表示抽象类(AbstractClass),C表示类(Class),$表示内部类。实线表示继承关系,虚线表示引用关系。本书中所有关于类的继承层次的图片都遵循这个图例。
Lock是一个接口,其定义如下:
常用的方法是lock/unlock。lock不能被中断,对应的lockinterruptibly可以被中断。
ReentrantLock本身没有代码逻辑,实现都在其内部类Sync中。
锁的公平性与非公平性
Sync是一个抽象类,它有两个子类FairSync与NonfairSync,分别对应公平锁和非公平锁。从下面的ReentrantLock构造函数可以看出,会传入一个布尔类型的变量fair指定锁是公平的还是非公平的,默认为非公平的。
什么叫公平锁和非公平锁呢?先举个现实生活中的例子,一个人去火车站售票窗口买票,发现现场有人排队,于是他排在队伍末尾,遵循先到者优先服务的规则,这叫公平;如果他去了不排队,直接冲到窗口买票,这叫作不公平。
对应到锁的例子,一个新的线程来了之后,看到有很多线程在排队,自己排到队伍末尾,这叫公平;线程来了之后直接去抢锁,这叫作不公平。不同于现实生活,这里默认设置的是非公平锁,其实是为了提高效率,减少线程切换。后面会详细地通过代码来对比公平锁和非公平锁在实现上的差异。
锁实现的基本原理
Sync的父类AbstractQueuedSynchronizer经常被称作队列同步器(AQS),这个类非常关键,下面会反复提到,该类的父类是AbstractOwnableSynchronizer。
上一章讲的Atomic类都是“自旋”性质的锁,而本章讲的锁将具备synchronized功能,也就是可以阻塞一个线程。为了实现一把具有阻塞或唤醒功能的锁,需要几个核心要素:
① 需要一个state变量,标记该锁的状态。state变量至少有两个值:0、1。对state变量的操作,要确保线程安全,也就是会用到CAS。
② 需要记录当前是哪个线程持有锁。
③ 需要底层支持对一个线程进行阻塞或唤醒操作。
④ 需要有一个队列维护所有阻塞的线程。这个队列也必须是线程安全的无锁队列,也需要用到CAS。
针对要素①②,在上面两个类中有对应的体现:
state取值不仅可以是0、1,还可以大于1,就是为了支持锁的可重入性。例如,同样一个线程,调用5次lock,state会变成5;然后调用5次unlock,state减为0。当state=0时,没有线程持有锁,exclusiveOwnerThread=null;当state=1时,有一个线程持有锁,exclusiveOwnerThread=该线程;
当state>;1时,说明该线程重入了该锁。
针对要素③,在Unsafe类中,提供了阻塞或唤醒线程的一对操作原语,也就是park/unpark。
有一个LockSupport的工具类,对这一对原语做了简单封装
在当前线程中调用park,该线程就会被阻塞;在另外一个线程中,调用unpark(Thread t),传入一个被阻塞的线程,就可以唤醒阻塞在park地方的线程。
尤其是 unpark(Thread t),它实现了一个线程对另外一个线程的“精准唤醒”。前面讲到的wait/notify,notify也只是唤醒某一个线程,但无法指定具体唤醒哪个线程。
针对要素④,在AQS中利用双向链表和CAS实现了一个阻塞队列。
如下所示。
阻塞队列是整个AQS核心中的核心,下面做进一步的阐述。如图3-2所示,head指向双向链表头部,tail指向双向链表尾部。入队就是把新的Node加到tail后面,然后对tail进行CAS操作;出队就是对head进行CAS操作,把head向后移一个位置。
初始的时候,head=tail=NULL;然后,在往队列中加入阻塞的线程时,会新建一个空的Node,让head和tail都指向这个空Node;之后,在后面加入被阻塞的线程对象。所以,当head=tail的时候,说明队列为空。
公平与非公平的lock的实现差异
下面分析基于AQS,ReentrantLock在公平性和非公平性上的实现差异。
acquire是AQS的一个模板方法,如下所示。
tryAcquire(..)是一个虚函数,也就是再次尝试拿锁,被NonfairSync与FairSync分别实现。acquireQueued(..)函数的目的是把线程放入阻塞队列,然后阻塞该线程。下面再分别来看FairSync与NonfairSync的tryAcquire(1)有什么区别。
这两段代码非常相似,唯一的区别是第二段代码多了一个if (!hasQueuedPredecessors)。什么意思呢?就是只有当c==0(没有线程持有锁),并且排在队列的第1个时(即当队列中没有其他线程的时候),才去抢锁,否则继续排队,这才叫“公平”!
阻塞队列与唤醒机制
下面进入锁的最为关键的部分,即acquireQueued(..)函数内部一探究竟。
先说addWaiter(..) 函数,就是为当前线程生成一个Node,然后把Node放入双向链表的尾部。要注意的是,这只是把Thread对象放入了一个队列中而已,线程本身并未阻塞。
在addWaiter(..)函数把Thread对象加入阻塞队列之后的工作就要靠acquireQueued(..)函数完成。线程一旦进入acquireQueued(..)就会被无限期阻塞,即使有其他线程调用interrupt函数也不能将其唤醒,除非有其他线程释放了锁,并且该线程拿到了锁,才会从accquireQueued(..)返回。
注意:进入acquireQueued(..),该线程被阻塞。在该函数返回的一刻,就是拿到锁的那一刻,也就是被唤醒的那一刻,此时会删除队列的第一个元素(head指针前移1个节点)。
首先,acquireQueued(..)函数有一个返回值,表示什么意思呢?虽然该函数不会中断响应,但它会记录被阻塞期间有没有其他线程向它发送过中断信号。如果有,则该函数会返回true;否则,返回false。
基于这个返回值,才有了下面的代码:
当 acquireQueued(..) 返回 true 时,会调用 selfInterrupt,自己给自己发送中断信号,也就是自己把自己的中断标志位设为true。之所以要这么做,是因为自己在阻塞期间,收到其他线程中断信号没有及时响应,现在要进行补偿。这样一来,如果该线程在lock代码块内部有调用sleep之类的阻塞方法,就可以抛出异常,响应该中断信号。
阻塞就发生在下面这个函数中:
线程调用 park函数,自己把自己阻塞起来,直到被其他线程唤醒,该函数返回。park函数返回有两种情况。
情况1:其他线程调用了unpark(Thread t)。
情况2:其他线程调用了t.interrupt。这里要注意的是,lock不能响应中断,但LockSupport.park会响应中断。
也正因为LockSupport.park可能被中断唤醒,acquireQueued(..)函数才写了一个for死循环。唤醒之后,如果发现自己排在队列头部,就去拿锁;如果拿不到锁,则再次自己阻塞自己。不断重复此过程,直到拿到锁。
被唤醒之后,通过Thread.Interrupted来判断是否被中断唤醒。如果是情况1,会返回false;如果是情况2,则返回true。
unlock实现分析
说完了lock,下面分析unlock的实现。unlock不区分公平还是非公平。
release里面做了两件事:tryRelease(..)函数释放锁;unparkSuccessor(..)函数唤醒队列中的后继者。
在上面的代码中有一个关键点要说明:因为是排他锁,只有已经持有锁的线程才有资格调用 release(..),这意味着没有其他线程与它争抢。所以,在上面的 tryRelease(..)函数中,对 state值的修改,不需要CAS操作,直接减1即可。
但对于读写锁中的读锁,也就是releaseShared(..),就不一样了,见后续分析。
lockInterruptibly实现分析
上面的 lock 不能被中断,这里的 lockInterruptibly可以被中断,下面看一下两者在实现上有什么差别。
这里的 acquireInterruptibly(..)也是 AQS 的模板方法,里面的 tryAcquire(..)分别被 FairSync和NonfairSync实现,此处不再重复叙述。这里主要讲doAcquireInterruptibly(..)函数。
明白了accquireQueued(..) 原理,此处就很简单了。当parkAndCheckInterrupt返回true的时候,说明有其他线程发送中断信号,直接抛出InterruptedException,跳出for循环,整个函数返回。
tryLock实现分析
tryLock实现基于调用非公平锁的tryAcquire(..),对state进行CAS操作,如果操作成功就拿到锁;如果操作不成功则直接返回false,也不阻塞。
来源:最亮的星ab