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

ReentrantLock可重入,可中断,锁超时实现原理

时间:2023-03-21 10:52:33 科技观察

概述上面解释了ReentrantLock加锁和解锁的原理,但是没有解释它的可重入,可中断,超时获取锁。本文主要针对这三种情况。建议您先阅读本文,了解ReentrantLock加锁的基本原理,并举例说明ReentrantLock公平锁和非公平锁的实现原理。Reentrant可重入的意思是如果一个线程获得了锁,它就是这个锁的所有者,它可以再次获得锁。这理解为可重入。总之,可以重复获取同一个锁,不会造成阻塞,例如:@TestpublicvoidtestRepeatLock(){//第一次获取锁reentrantLock.lock();try{System.out.println(Thread.currentThread().getName()+"先获取锁");//再次获取锁tryAgainLock(reentrantLock);}最后{reentrantLock.unlock();}}publicvoidtryAgainLock(ReentrantLockreentrantLock){//第二次获取锁reentrantLock.lock();try{System.out.println(Thread.currentThread().getName()+"第二次获取锁");}最后{reentrantLock.unlock();}}同一个线程多次使用ReentrantLock获取锁不会阻塞几次锁的申请,最后需要释放几次锁。你知道如何实现吗?ReentrantLock的整个加锁和解锁过程已经在概述文章中进行了说明,重入实现在里面。这里重点关注申请锁的tryAcquire方法,最后调用nonfairTryAcquire方法。如果一个线程已经获取了锁,并且占用锁的线程是当前线程,则说明【发生了锁重入】。上图第1步计算nextc的次数等于当前次数+新增次数,acquires等于1updatestate的值,这里没有用cas,因为当前线程是持有锁,所以这里的操作相当于在监听,然后返回true,表示再次申请锁成功。可中断ReentrantLock相对于synchronized锁的一大优势就是可以被中断,那么什么是可中断呢?ReentrantLock是通过lockInterruptibly()来锁定的。如果无法获取到锁,可以通过调用线程的interrupt()提前终止线程。例如:@TestpublicvoidtestInterrupt()throwsInterruptedException{ReentrantLocklock=newReentrantLock();//主线程一般加锁System.out.println("主线程先获取锁");锁.锁();try{//创建子线程Threadt1=newThread(()->{try{System.out.println("t1尝试获取中断锁");lock.lockInterruptibly();}catch(InterruptedExceptione){System.out.println("t1没有获取锁,被中断,直接返回");return;}try{System.out.println("t1成功获取锁");}finally{System.out.println("t1释放锁");lock.unlock();}},"t1");t1.开始();线程.睡眠(2000);System.out.println("主线程中断锁");t1.中断();}finally{//主线程解锁System.out.println("主线程先释放锁");lock.unlock();}}在通过lockInterruptibly()方法获取锁期间,可以通过线程的interrupt()方法中断,跳出通过lock()方法阻塞并获取锁,没有响应interrupt()方法的中断,接下来我们看一下它的实现原理publicvoidlockInterruptibly()throwsInterruptedException{sync.acquireInterruptibly(1);}publicfinalvoidacquireInterruptibly(intarg){//被其他线程中断直接returnfalseif(Thread.interrupted())thrownewInterruptedException();if(!tryAcquire(arg))//没有获取锁,进入这里doAcquireInterruptibly(arg);}首先判断线程是否被中断,如果没有,则直接抛出中断异常,如果没有获取到锁,则调用doAcquireInterruptibly()方法。privatevoiddoAcquireInterruptibly(intarg)throwsInterruptedException{//封装当前线程加入队列finalNodenode=addWaiter(Node.EXCLUSIVE);布尔失败=真;try{//spinfor(;;){//shouldParkAfterFailedAcquire判断是否阻塞等待//parkAndCheckInterrupt方法阻塞线程,返回true,表示线程中断if(shouldParkAfterFailedAcquire(p,node)&&parkAndCheckInterrupt())//【在park过程中被打断会抛出异常】,而不是再次进入循环获取锁来完成打断效果thrownewInterruptedException();}}finally{//在抛出异常之前会进入这里if(failed)//取消当前线程的节点cancelAcquire(node);}}addWaiter将当前线程封装成一个节点,加入到队列中。shouldParkAfterFailedAcquire()方法判断如果前一个节点的等待状态为-1,则返回true,说明当前线程需要阻塞。parkAndCheckInterrupt()方法阻塞线程并返回true,表示线程被中断,抛出InterruptedException。最后调用cancelAcquire()方法将当前节点状态设置为取消状态。//取消节点出队逻辑privatevoidcancelAcquire(Nodenode){//判断为空if(node==null)return;//将当前节点封装的Thread设置为nullnode.thread=null;//获取当前取消节点的前驱节点Nodepred=node.prev;//前驱节点也被取消,循环寻找最近的非取消节点while(pred.waitStatus>0)node.prev=pred=pred.prev;//获取前驱节点的后继节点,可能是当前节点,也可能是waitStatus>0的节点NodepredNext=pred.next;//设置当前节点的状态为[取消状态1]node.waitStatus=Node.CANCELLED;//如果条件成立,则表示当前节点为尾节点,设置当前节点的前驱节点为尾节点if(node==tail&&compareAndSetTail(node,pred)){//使前驱节点的后继节点为空,这里直接Dequeue所有被取消的节点compareAndSetNext(pred,predNext,null);}else{//表示当前节点不是尾节点intws;//条件1表示当前节点不是head.next节点if(pred!=head&&//判断前驱节点的状态是否为-1,如果不成立,说明当前节点的状态precursor可能为0或者刚刚被其他线程取消((ws=pred.waitStatus)==Node.SIGNAL||//如果状态不为-1,则将前驱节点的状态设置为-1(ws<=0&&我;compareAndSetWaitStatus(pred,ws,Node.SIGNAL)))&&//前驱节点的线程不为空??pred.thread!=null){Nodenext=node.next;//当前节点的后继节点为普通节点if(next!=null&&next.waitStatus<=0)//设置前驱节点的后继节点为当前节点的后继节点,[当前节点从队列中删除]compareAndSetNext(pred,predNext,next);}else{//当前节点为head.next节点,唤醒当前节点的后继节点unparkSuccessor(node);}node.next=节点;//helpGC}}LocktimeoutReentrantLock也有加锁超时的功能,调用tryLock(longtimeout,TimeUnitunit)方法,在给定时间内获取锁,没有获取到锁则退出,也是一个函数同步没有@TestpublicvoidtestLockTimeout()throwsInterruptedException{ReentrantLocklock=newReentrantLock();Threadt1=newThread(()->{try{//调用tryLock获取锁if(!lock.tryLock(2,TimeUnit.SECONDS)){System.out.println("t1无法获取锁");return;}}catch(InterruptedExceptione){System.out.println("t1被中断,获取不到锁");return;}try{System.out.println("t1获取到锁");}最后{lock.unlock();}},"t1");//主线程锁lock.lock();System.out.println("主线程获得了锁");t1.开始();线程.睡眠(3000);try{System.out.println("主线程释放了锁");}最后{lock.unlock();那么这个原理的实现是什么呢?publicbooleantryLock(longtimeout,TimeUnitunit)throwsInterruptedException{//调用tryAcquireNanos方法returnsync.tryAcquireNanos(1,unit.toNanos(timeout));}publicfinalbooleantryAcquireNanos(intarg,longnanosTimeout){if(Thread.interrupted())thrownewInterruptedException();//tryAcquire尝试一次,如果获取不到,调用doAcquireNanos方法returntryAcquire(arg)||doAcquireNanos(arg,nanosTimeout);}protectedfinalbooleantryAcquire(intacquires){返回nonfairTryAcquire(acquires);longnanosTimeout){如果(nanosTimeout<=0L)返回false;//获取截止时间的时间戳finallongdeadline=System.nanoTime()+nanosTimeout;//将当前线程加入队列finalNodenode=addWaiter(Node.EXCLUSIVE);布尔失败=真;try{//spinfor(;;){//获取前驱节点finalNodep=node.predecessor();//前驱节点为head,尝试获取锁if(p==head&&tryAcquire(arg)){setHead(node);p.next=null;//帮助GCfailed=false;返回真;}//计算等待时间nanosTimeout=deadline-System.nanoTime();if(nanosTimeout<=0L)//时间到了returnfalse;if(shouldParkAfterFailedAcquire(p,node)&&//如果nanosTimeout大于这个值,阻塞才有意义,否则直接自旋会更好nanosTimeout>spinForTimeoutThreshold)LockSupport.parkNanos(this,nanosTimeout);//[Interrupted会报异常]if(Thread.interrupted())thrownewInterruptedException();}}}如果nanosTimeout小于0,说明在指定时间还没有成功获取锁,返回false如果nanosTimeout大于spinForTimeoutThreshold,则值为1000L,阻塞是因为阻塞是没有意义的太短了,不然直接旋转会更好。小结本文主要讲解ReentrantLock锁从使用到原理的可重入、可中断和锁超时特性。希望对大家有所帮助。

最新推荐
猜你喜欢