当前位置: 首页 > 科技观察

ReentrantLock的核心原理,绝对干货

时间:2023-03-12 16:25:50 科技观察

前言下面是一道面试题:面试官:我看你对ReentrantLock的源码很熟悉,能说说他的中断锁是怎么实现的吗?不知道没关系,看完这篇文章,通过思考就能找到答案,下面开始吧。ReentrantLock中文叫做可重入互斥量。可重入是指同一个线程可以重复锁定或释放同一个共享资源。互斥是AQS中的独占锁,意思是只允许一个线程获取锁。简单应用ReentrantLock的使用比synchronized稍微麻烦一点。所谓显示锁,就是需要你在代码中主动进行锁操作。一般来说,我们可以通过如下方式使用ReentrantLockLocklock=newReentrantLock();lock.lock();try{doSomething();}finally{lock.unlock();}lock.lock()来显式加锁。加锁后,下面的代码块必须放在try中,必须结合finally代码块调用lock.unlock()来释放锁。否则,如果doSomething方法出现任何异常,锁永远不会被释放。公平锁和非公平锁synchronized是非公平锁,就是说每当key被释放后,所有等待锁的线程都不会按照队列的顺序依次获取锁,而是会重新竞争锁。ReentrantLock相比之下更加灵活,它可以同时支持公平锁和非公平锁。声明时传递true即可。锁锁=newReentrantLock(真);并且默认的无参数构造方法将创建一个不公平的锁。在tryLock方法之前,我们使用了lock.lock();完成锁定。此时加锁操作被阻塞,直到获取到锁才会继续。ReentrantLock其实还有一个更灵活的锁链方法tryLock。tryLock方法有两个重载。第一个是没有参数的tryLock方法。该方法被调用后会立即返回获取锁。为真,不为假。我们的代码可以使用返回的结果进行进一步处理。第二种是带参数的tryLock方法,通过传入时间和单位来控制等待锁的时间长短。如果在限定时间内未能获取到锁,则返回false,否则返回true。用法如下:if(lock.tryLock(2,TimeUnit.SECONDS)){try{doSomething();}catch(InterruptedExceptione){e.printStackTrace();}finally{lock.unlock();}}else{doSomethingElse();}如果我们不想在拿不到锁的时候一直等待,而是希望能够做一些其他的事情,可以选择这种方式。类结构ReentrantLock类本身不继承AQS,实现了Lock接口,如下:publicclassReentrantLockimplementsLock,java.io.Serializable{}Lock接口定义了各种加锁和释放锁的方法,接口如下://GetLock方法,得不到锁的线程会去同步队列阻塞排队立即为true,否则返回falsebooleantryLock();//有超时等待时间的锁。如果超时仍未获取到锁,则返回falsebooleantryLock(longtime,TimeUnitunit)throwsInterruptedException;//释放锁voidunlock();//获取新的ConditionConditionnewCondition();ReentrantLock负责实现这些接口。我们在使用的时候直接面对这些方法。这些方法的底层实现都交给了Sync内部类来实现。Sync类的定义如下:abstractstaticclassSyncextendsAbstractQueuedSynchronizer{}最终继承自AbstractQueuedSynchronizer。这就是著名的AQS。通过查看AQS的注释,了解到AQS依赖先进先出队列实现阻塞锁和相关同步器(信号量、事件等)。AQS内部有一个volatile状态属性。多线程对锁的竞争其实体现在写状态值的竞争上。一旦状态从0变为1,就说明有线程已经竞争锁,其他线程进入等待队列。等待队列是一个链表结构的FIFO队列,可以保证公平锁的实现。当同一个线程多次获取锁时,如果该线程之前已经持有过锁,则再次将状态加1。当锁被释放时,state-1将被检查。直到减为0,才表示线程真正释放了锁。Sync继承了AbstractQueuedSynchronizer,所以Sync有一个锁框架。按照AQS框架,Sync只需要实现AQS保留的几个方法,但是Sync只实现了部分方法,部分方法交给子类NonfairSync和FairSync来实现,NonfairSync是非公平锁,FairSync是公平锁lock,定义如下://同步器Sync的两个子类锁SyncstaticfinalclassFairSyncextendsSync{}staticfinalclassNonfairSyncextendsSync{}几个类的整体结构如下:图中的Sync、NonfairSync、FairSync都是以staticinner的形式实现的类,也符合AQS框架定义的实现标准。ConstructorReentrantLock有两个构造函数,代码如下://无参构造函数,相当于ReentrantLock(false),默认不公平newFairSync():newNonfairSync();}无参构造函数默认构造的是非公平锁,有参构造函数可以选择。从构造函数可以看出公平锁是通过FairSync实现的,非公平锁是通过源码分析NonfairSync实现的。,交给FairSync和NonfairSync的两个子类来实现。加锁方法FairSync公平锁FairSync公平锁只实现了lock和tryAcquire两个方法。锁的方法很简单,如下://acquire是AQS的方法,意思是先尝试获取锁,失败后进入同步队列阻塞等待finalvoidlock(){acquire(1);}不会覆盖FairSync中的获取方法代码。调用AbstractQueuedSynchronizer的代码如下:publicfinalvoidacquire(intarg){if(!tryAcquire(arg)&&acquireQueued(addWaiter(Node.EXCLUSIVE),arg))selfInterrupt();}先调用一次tryAcquire方法。如果tryAcquire方法返回true,则acquire立即返回。但是如果tryAcquire返回false,那么会先调用addWaiter将当前线程包装成一个等待节点,加入到等待队列中。然后调用acquireQueued尝试排队获取锁。如果成功后发现自己被打断了,则返回true,导致selfInterrupt被触发。在这个方法中,只要调用Thread.currentThread().interrupt();打断。acquireQueued的代码如下:在这个方法中进入自旋,不断的检查你队列的状态。如果轮到你了(header是已经获取锁的线程,header后面的线程是排队等待获取锁的线程),那么调用tryAcquire方法获取锁,然后自己设置作为队列的头部。在自旋中,它还会检查如果它没有排队到自己,是否应该被中断。我们可以总结一下获取锁的整个过程:直接通过tryAcquire尝试获取锁,成功直接返回;如果获取失败,则将自己加入等待队列;旋转以检查您的队列状态;如果轮到你排队了,那就尝试通过tryAcquire获取锁;如果还没轮到你,则返回第三步查看你的排队情况。从上面的流程可以看出,锁是通过tryAcquire方法获取的。并且这个方法在FairSync和NonfairSync中有不同的实现。这个tryAcquire方法是AQS留给acquire方法中子类实现的抽象方法。FairSync中实现的源码如下:其实它的实现和NonfairSync是一样的,只是当c==0时,多了一个hasQueuedPredecessors方法的调用。顾名思义,该方法的作用是判断当前线程之前是否有排队线程。当前面没有排队的线程时,说明已经排队给自己了,然后通过CAS把state值改为1。如果成功,说明当前线程已经成功获取到锁。下一步是调用setExclusiveOwnerThread将自己设置为锁的所有者。elseif中的逻辑是处理重入逻辑。如果当前线程是锁的所有者,则更新状态加1。通过上面的分析可以看出,AbstractQueuedSynchronizer提供了acquire方法的模板逻辑,但是真正的锁获取方法tryAcquire是在不同的子类中实现的,这是一个很好的设计理念。NonfairSync非公平锁NonfairSync底层实现了lock和tryAcquire两个方法,如下:nonfairTryAcquire上面代码中需要注意三点:通过判断AQS的状态来判断能否获得锁,0表示锁是免费的;elseif的代码体现了可重入锁。同一个线程可重入地锁定共享资源。底层实现是加state+1,重入次数有限制,为Integer的最大值;这种方法是不公平的。所以只用了非公平锁,公平锁是另一种实现方式。该方法由不带参数的tryLock方法调用。tryLock方法源码如下:publicbooleantryLock(){//入参为1尝试获取锁returnssync.nonfairTryAcquire(1);}底层调用关系(只是简单表示调用关系,不是a完整的分支图)如下:unlock方法publicvoidunlock(){sync.release(1);}和lock很相似,实际上调用了sync实现类的release方法。和lock方法一样,这个release方法在AbstractQueuedSynchronizer中,if(tryRelease(arg)){Nodeh=head;if(h!=null&&h.waitStatus!=0)unparkSuccessor(h);returntrue;}returnfalse;这个方法会执行首先是tryRelease,它的实现也在AbstractQueuedSynchronizer的子类Sync中。如果锁释放成功,会通过unparkSuccessor方法找到队列中第一个waitStatus<0的线程唤醒。我们看一下tryRelease方法的代码:tryRelease方法对于公平锁和非公平锁都是通用的。释放锁的时候,没有公平不公平之分。从代码中可以看出,最终释放锁的意思是状态state为0,重入锁的情况下,锁需要重入并解锁相应次数后才能加锁终于可以放出来了。比如线程A共享资源B重入加锁5次,那么如果释放锁,需要释放5次才能真正释放共享资源。本文总结了ReentrantLock的使用及其核心源码。其实和Lock相关的代码还有很多。我们可以尝试自己阅读。ReentrantLock的设计思想是通过FIFO队列来保存等待锁的线程。通过volatile类型的状态保存持有锁的数量,从而实现锁的重入。公平锁是通过判断是否排队成功来决定是否竞争锁。然后我们了解到AQS搭建了整个锁的架构。子类锁只需要根据场景实现AQS对应的方法即可。不仅ReentrantLock是这样,JUC中的其他锁也是这样。只要熟悉AQS,lock其实很简单。本文转载自微信公众号“月亮与飞鱼”,您可以通过以下二维码关注。转载本文请联系月版飞语公众号。