在开始本篇文章的内容讲述前,先来回答我一个问题,为何 JDK 提供一个 synchronized
关键字以后还要提供一个 Lock 锁,这不是画蛇添足吗?难道 JDK 设计人员都是沙雕吗?html
我听过一句话很是的经典,也是我认为是每一个人都应该了解的一句话:你觉得的并非你觉得的
。明白什么意思么?不明白的话,加我微信我告诉你。java
ReentrantLock 位于 java.util.concurrent.locks
包下,它实现了 Lock
接口和 Serializable
接口。面试
ReentrantLock 是一把可重入锁
和互斥锁
,它具备与 synchronized 关键字相同的含有隐式监视器锁(monitor)的基本行为和语义,可是它比 synchronized 具备更多的方法和功能。微信
ReentrantLock 类中带有两个构造函数,一个是默认的构造函数,不带任何参数;一个是带有 fair 参数的构造函数数据结构
public ReentrantLock() { sync = new NonfairSync(); } public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
第二个构造函数也是判断 ReentrantLock 是不是公平锁的条件,若是 fair 为 true,则会建立一个公平锁
的实现,也就是 new FairSync()
,若是 fair 为 false,则会建立一个 非公平锁
的实现,也就是 new NonfairSync()
,默认的状况下建立的是非公平锁多线程
// 建立的是公平锁 private ReentrantLock lock = new ReentrantLock(true); // 建立的是非公平锁 private ReentrantLock lock = new ReentrantLock(false); // 默认建立非公平锁 private ReentrantLock lock = new ReentrantLock();
FairSync 和 NonfairSync 都是 ReentrantLock 的内部类,继承于 Sync
类,下面来看一下它们的继承结构,便于梳理。并发
abstract static class Sync extends AbstractQueuedSynchronizer {...} static final class FairSync extends Sync {...} static final class NonfairSync extends Sync {...}
在多线程尝试加锁时,若是是公平锁,那么锁获取的机会是相同的。不然,若是是非公平锁,那么 ReentrantLock 则不会保证每一个锁的访问顺序。函数
下面是一个公平锁
的实现工具
public class MyFairLock extends Thread{ private ReentrantLock lock = new ReentrantLock(true); public void fairLock(){ try { lock.lock(); System.out.println(Thread.currentThread().getName() + "正在持有锁"); }finally { System.out.println(Thread.currentThread().getName() + "释放了锁"); lock.unlock(); } } public static void main(String[] args) { MyFairLock myFairLock = new MyFairLock(); Runnable runnable = () -> { System.out.println(Thread.currentThread().getName() + "启动"); myFairLock.fairLock(); }; Thread[] thread = new Thread[10]; for(int i = 0;i < 10;i++){ thread[i] = new Thread(runnable); } for(int i = 0;i < 10;i++){ thread[i].start(); } } }
不信?不信你输出试试啊!懒得输出?就知道你懒得输出,因此直接告诉你结论吧,结论就是本身试
。源码分析
试完了吗?试完了我是不会让你休息的,过来再试一下非公平锁的测试和结论,知道怎么试吗?上面不是讲过要给 ReentrantLock 传递一个参数的吗?你想,传 true 的时候是公平锁,那么反过来不就是非公平锁了?其余代码还用改吗?不须要了啊。
明白了吧,再来测试一下非公平锁的流程,看看是否是你想要的结果。
一般状况下,使用多线程访问公平锁的效率会很是低
(一般状况下会慢不少),可是 ReentrantLock 会保证每一个线程都会公平的持有锁,线程饥饿的次数比较小
。锁的公平性并不能保证线程调度的公平性。
此时若是你想了解更多的话,那么我就从源码的角度跟你聊聊如何 ReentrantLock 是如何实现这两种锁的。
如上图所示,公平锁的加锁流程要比非公平锁的加锁流程简单,下面要聊一下具体的流程了,请小伙伴们备好板凳。
下面先看一张流程图,这张图是 acquire 方法的三条主要流程
首先是第一条路线,tryAcquire 方法,顾名思义尝试获取,也就是说能够成功获取锁,也能够获取锁失败。
使用 ctrl+左键
点进去是调用 AQS 的方法,可是 ReentrantLock 实现了 AQS 接口,因此调用的是 ReentrantLock 的 tryAcquire 方法;
首先会取得当前线程,而后去读取当前锁的同步状态,还记得锁的四种状态吗?分别是 无锁、偏向锁、轻量级锁和重量级锁
,若是你不是很明白的话,请参考博主这篇文章(不懂什么是锁?看看这篇你就明白了),若是判断同步状态是 0 的话,就证实是无锁的,参考下面这幅图( 1bit 表示的是是否偏向锁 )
若是是无锁(也就是没有加锁),说明是第一次上锁,首先会先判断一下队列中是否有比当前线程等待时间更长的线程(hasQueuedPredecessors);而后经过 CAS
方法原子性的更新锁的状态,CAS 方法更新的要求涉及三个变量,currentValue(当前线程的值),expectedValue(指望更新的值),updateValue(更新的值)
,它们的更新以下
if(currentValue == expectedValue){ currentValue = updateValue }
CAS 经过 C 底层机制保证原子性,这个你不须要考虑它。若是既没有排队的线程并且使用 CAS 方法成功的把 0 -> 1 (偏向锁),那么当前线程就会得到偏向锁,记录获取锁的线程为当前线程。
而后咱们看 else if
逻辑,若是读取的同步状态是1,说明已经线程获取到了锁,那么就先判断当前线程是否是获取锁的线程,若是是的话,记录一下获取锁的次数 + 1,也就是说,只有同步状态为 0 的时候是无锁状态。若是当前线程不是获取锁的线程,直接返回 false。
acquire 方法会先查看同步状态是否获取成功,若是成功则方法结束返回,也就是 !tryAcquire == false
,若失败则先调用 addWaiter 方法再调用 acquireQueued 方法
而后看一下第二条路线 addWaiter
这里首先把当前线程和 Node 的节点类型进行封装,Node 节点的类型有两种,EXCLUSIVE
和 SHARED
,前者为独占模式,后者为共享模式,具体的区别咱们会在 AQS 源码讨论,这里读者只须要知道便可。
首先会进行 tail 节点的判断,有没有尾节点,其实没有头节点也就至关于没有尾节点,若是有尾节点,就会原子性的将当前节点插入同步队列中,再执行 enq 入队操做,入队操做至关于原子性的把节点插入队列中。
若是当前同步队列尾节点为null,说明当前线程是第一个加入同步队列进行等待的线程。
在看第三条路线 acquireQueued
主要会有两个分支判断,首先会进行无限循环中,循环中每次都会判断给定当前节点的先驱节点,若是没有先驱节点会直接抛出空指针异常,直到返回 true。
而后判断给定节点的先驱节点是否是头节点,而且当前节点可否获取独占式锁,若是是头节点而且成功获取独占锁后,队列头指针用指向当前节点,而后释放前驱节点。若是没有获取到独占锁,就会进入 shouldParkAfterFailedAcquire
和 parkAndCheckInterrupt
方法中,咱们贴出这两个方法的源码
shouldParkAfterFailedAcquire
方法主要逻辑是使用compareAndSetWaitStatus(pred, ws, Node.SIGNAL)
使用CAS将节点状态由 INITIAL 设置成 SIGNAL,表示当前线程阻塞。当 compareAndSetWaitStatus 设置失败则说明 shouldParkAfterFailedAcquire 方法返回 false,而后会在 acquireQueued 方法中死循环中会继续重试,直至compareAndSetWaitStatus 设置节点状态位为 SIGNAL 时 shouldParkAfterFailedAcquire 返回 true 时才会执行方法 parkAndCheckInterrupt 方法。(这块在后面研究 AQS 会细讲)
parkAndCheckInterrupt
该方法的关键是会调用 LookSupport.park 方法(关于LookSupport会在之后的文章进行讨论),该方法是用来阻塞当前线程。
因此 acquireQueued 主要作了两件事情:若是当前节点的前驱节点是头节点,而且可以获取独占锁,那么当前线程可以得到锁该方法执行结束退出
若是获取锁失败的话,先将节点状态设置成 SIGNAL,而后调用 LookSupport.park
方法使得当前线程阻塞。
若是 !tryAcquire
和 acquireQueued
都为 true 的话,则打断当前线程。
那么它们的主要流程以下(注:只是加锁流程,并非 lock 全部流程)
非公平锁的加锁步骤和公平锁的步骤只有两处不一样,一处是非公平锁在加锁前会直接使用 CAS 操做设置同步状态,若是设置成功,就会把当前线程设置为偏向锁的线程;一处是 CAS 操做失败执行 tryAcquire
方法,读取线程同步状态,若是未加锁会使用 CAS 再次进行加锁,不会等待 hasQueuedPredecessors
方法的执行,达到只要线程释放锁就会加锁的目的。下面经过源码和流程图来详细理解
这是非公平锁和公平锁不一样的两处地方,下面是非公平锁的加锁流程图
如下是 JavaDoc 官方解释:
lockInterruptibly 的中文意思为若是没有被打断,则获取锁。若是没有其余线程持有该锁,则获取该锁并当即返回,将锁保持计数设置为1。若是当前线程已经持有锁,那么此方法会马上返回而且持有锁的数量会 + 1。若是锁是由另外一个线程持有的,则出于线程调度目的,当前线程将被禁用,并处于休眠状态,直到发生如下两种状况之一
若是当前线程获取了锁,则锁保持计数将设置为1。
若是当前线程发生了以下状况:
那么当前线程就会抛出InterruptedException
而且当前线程的中断状态会清除。
下面看一下它的源码是怎么写的
首先会调用 acquireInterruptibly
这个方法,判断当前线程是否被中断,若是中断抛出异常,没有中断则判断公平锁/非公平锁
是否已经获取锁,若是没有获取锁(tryAcquire 返回 false)则调用 doAcquireInterruptibly
方法,这个方法和 acquireQueued 方法没什么区别,就是线程在等待状态的过程当中,若是线程被中断,线程会抛出异常。
下面是它的流程图
仅仅当其余线程没有获取这把锁的时候获取这把锁,tryLock 的源代码和非公平锁的加锁流程基本一致,它的源代码以下
ReentrantLock
除了能以中断的方式去获取锁,还能够以超时等待的方式去获取锁,所谓超时等待就是线程若是在超时时间内没有获取到锁,那么就会返回false
,而不是一直死循环获取。可使用 tryLock 和 tryLock(timeout, unit)) 结合起来实现公平锁,像这样
if (lock.tryLock() || lock.tryLock(timeout, unit)) {...}
若是超过了指定时间,则返回值为 false。若是时间小于或者等于零,则该方法根本不会等待。
它的源码以下
首先须要了解一下 TimeUnit
工具类,TimeUnit 表示给定粒度单位的持续时间,而且提供了一些用于时分秒跨单位转换的方法,经过使用这些方法进行定时和延迟操做。
toNanos
用于把 long 型表示的时间转换成为纳秒,而后判断线程是否被打断,若是没有打断,则以公平锁/非公平锁
的方式获取锁,若是可以获取返回true,获取失败则调用doAcquireNanos
方法使用超时等待的方式获取锁。在超时等待获取锁的过程当中,若是等待时间大于应等待时间,或者应等待时间设置不合理的话,返回 false。
这里面以超时的方式获取锁也能够画一张流程图以下
unlock
和 lock
是一对情侣,它们分不开彼此,在调用 lock 后必须经过 unlock 进行解锁。若是当前线程持有锁,在调用 unlock 后,count 计数将减小。若是保持计数为0就会进行解锁。若是当前线程没有持有锁,在调用 unlock 会抛出 IllegalMonitorStateException
异常。下面是它的源码
在有了上面阅读源码的经历后,相信你会很快明白这段代码的意思,锁的释放不会区分公平锁仍是非公平锁,主要的判断逻辑就是 tryRelease
方法,getState
方法会取得同步锁的重入次数,若是是获取了偏向锁,那么可能会屡次获取,state 的值会大于 1,这时候 c 的值 > 0 ,返回 false,解锁失败。若是 state = 1,那么 c = 0,再判断当前线程是不是独占锁的线程,释放独占锁,返回 true,当 head 指向的头结点不为 null,而且该节点的状态值不为0的话才会执行 unparkSuccessor 方法,再进行锁的获取。
在多线程同时访问时,ReentrantLock 由最后一次
成功锁定的线程拥有,当这把锁没有被其余线程拥有时,线程调用 lock()
方法会马上返回并成功获取锁。若是当前线程已经拥有锁,这个方法会马上返回。能够经过 isHeldByCurrentThread
和 getHoldCount
来进行检查。
首先来看 isHeldByCurrentThread 方法
public boolean isHeldByCurrentThread() { return sync.isHeldExclusively(); }
根据方法名能够略知一二,是否被当前线程持有
,它用来询问锁是否被其余线程拥有,这个方法和 Thread.holdsLock(Object)
方法内置的监视器锁相同,而 Thread.holdsLock(Object) 是 Thread
类的静态方法,是一个 native
类,它表示的意思是若是当前线程在某个对象上持有 monitor lock(监视器锁) 就会返回 true。这个类没有实际做用,仅仅用来测试和调试所用。例如
private ReentrantLock lock = new ReentrantLock(); public void lock(){ assert lock.isHeldByCurrentThread(); }
这个方法也能够确保重入锁可以表现出不可重入
的行为
private ReentrantLock lock = new ReentrantLock(); public void lock(){ assert !lock.isHeldByCurrentThread(); lock.lock(); try { // 执行业务代码 }finally { lock.unlock(); } }
若是当前线程持有锁则 lock.isHeldByCurrentThread() 返回 true,不然返回 false。
咱们在了解它的用法后,看一下它内部是怎样实现的,它内部只是调用了一下 sync.isHeldExclusively(),sync
是 ReentrantLock 的一个静态内部类
,基于 AQS 实现,而 AQS 它是一种抽象队列同步器,是许多并发实现类的基础,例如 ReentrantLock/Semaphore/CountDownLatch。sync.isHeldExclusively() 方法以下
protected final boolean isHeldExclusively() { return getExclusiveOwnerThread() == Thread.currentThread(); }
此方法会在拥有锁以前先去读一下状态,若是当前线程是锁的拥有者,则不须要检查。
getHoldCount()
方法和isHeldByCurrentThread
都是用来检查线程是否持有锁的方法,不一样之处在于 getHoldCount() 用来查询当前线程持有锁的数量,对于每一个未经过解锁操做匹配的锁定操做,线程都会保持锁定状态,这个方法也一般用于调试和测试,例如
private ReentrantLock lock = new ReentrantLock(); public void lock(){ assert lock.getHoldCount() == 0; lock.lock(); try { // 执行业务代码 }finally { lock.unlock(); } }
这个方法会返回当前线程持有锁的次数,若是当前线程没有持有锁,则返回0。
ReentrantLock 能够经过 newCondition
方法建立 ConditionObject 对象,而 ConditionObject 实现了 Condition
接口,关于 Condition 的用法咱们后面再讲。
查询是否有任意线程已经获取锁,这个方法用来监视系统状态,而不是用来同步控制,很简单,直接判断 state
是否等于0。
这个方法也比较简单,直接使用 instanceof
判断是否是 FairSync
内部类的实例
public final boolean isFair() { return sync instanceof FairSync; }
判断同步状态是否为0,若是是0,则没有线程拥有锁,若是不是0,直接返回获取锁的线程。
final Thread getOwner() { return getState() == 0 ? null : getExclusiveOwnerThread(); }
判断是否有线程正在等待获取锁,若是头节点与尾节点不相等,说明有等待获取锁的线程。
public final boolean hasQueuedThreads() { return head != tail; }
判断给定的线程是否正在排队,若是正在排队,返回 true。这个方法会遍历队列,若是找到匹配的线程,返回true
public final boolean isQueued(Thread thread) { if (thread == null) throw new NullPointerException(); for (Node p = tail; p != null; p = p.prev) if (p.thread == thread) return true; return false; }
此方法会返回一个队列长度的估计值,该值只是一个估计值,由于在此方法遍历内部数据结构时,线程数可能会动态变化。 此方法设计用于监视系统状态,而不用于同步控制。
public final int getQueueLength() { int n = 0; for (Node p = tail; p != null; p = p.prev) { if (p.thread != null) ++n; } return n; }
返回一个包含可能正在等待获取此锁的线程的集合。 由于实际的线程集在构造此结果时可能会动态更改,因此返回的集合只是一个大概的列表集合。 返回的集合的元素没有特定的顺序。
public final Collection<Thread> getQueuedThreads() { ArrayList<Thread> list = new ArrayList<Thread>(); for (Node p = tail; p != null; p = p.prev) { Thread t = p.thread; if (t != null) list.add(t); } return list; }
那么你看完源码分析后,你能总结出 synchronized
和 lock
锁的实现 ReentrantLock
有什么异同吗?
Synchronzied 和 Lock 的主要区别以下:
Lock 锁能够提升多个线程进行读的效率(使用 readWriteLock)
面试官可能还会问你 ReentrantLock 的加锁流程是怎样的,其实若是你能把源码给他讲出来的话,必定是高分。若是你记不住源码流程的话能够记住下面这个简化版的加锁流程
若是锁数量为0,再基于 CAS 尝试将 state(锁数量)从0设置为1一次,若是设置成功,设置当前线程为独占锁的线程;
若是锁数量不为0或者上边的尝试又失败了,查看当前线程是否是已是独占锁的线程了,若是是,则将当前的锁数量+1;若是不是,则将该线程封装在一个Node内,并加入到等待队列中去。等待被其前一个线程节点唤醒。
文章参考:
【试验局】ReentrantLock中非公平锁与公平锁的性能测试
第五章 ReentrantLock源码解析1--得到非公平锁与公平锁lock()
https://juejin.im/post/5c95df...