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

ReentrantLock的条件变量Condition机制示意图

时间:2023-03-15 19:58:13 科技观察

概述大家肯定都用过wait()和notify()这两个方法。这两个方法主要用于多线程间的协同处理,即控制线程间的等待。、通知、切换和唤醒。ReentrantLock也支持这种条件变量的能力,而且比synchronized更强大,可以支持多个条件变量。ReentrantLock条件变量使用ReentrantLock类APIConditionnewCondition():创建条件变量对象Condition类APIvoidawait():当前线程从运行态进入等待态同时释放锁,该方法可以中断voidawaitUninterruptibly():当前线程从运行状态开始进入等待状态,此方法不可中断voidsignal():唤醒一个在条件队列上等待的线程TestpublicvoidtestCondition()throwsInterruptedException{ReentrantLocklock=newReentrantLock();//创建一个新的条件变量Conditioncondition=lock.newCondition();Threadthread0=newThread(()->{lock.lock();try{System.out.println("Thread0Acquirethelock");//sleep不会释放锁Thread.sleep(500);//进入休息室等待System.out.println("线程0释放锁,进入等待");condition.await();System.out.println("线程0被唤醒");}catch(InterruptedExceptione){e.printStackTrace();}最后{lock.unlock();}});thread0.start();//唤醒Threadthread1=newThread(()->{lock.lock();try{System.out.println("线程1获取锁");//唤醒条件.signal();System.out.println("线程1唤醒线程0");}最后{lock.unlock();System.out.println("线程1释放锁");}});thread1.start();thread0.join();thread1.join();}运行结果:条件的等待和通知必须在锁的范围内实现对于条件变量的等待和唤醒,必须是同一个条件。线程1执行conidtion.notify()后,锁并没有释放。需要等待锁被释放,线程0重新获取锁成功后,才能继续向下执行。图解实现原理await进程线程0(Thread-0)一开始就获取到锁,exclusiveOwnerThread字段为Thread-0。下图中深蓝色节点Thread-0调用了await方法。Thread-0被封装成一个Node,进入ConditionObject队列,因为此时只有一个节点,所有的firstWaiter和lastWaiter都指向Thread-0,锁资源会被释放,NofairSync中的状态会变为0,exclusiveOwnerThread设置为null。如下所示。线程1(Thread-1)被唤醒,重新获取锁,如下图深蓝色节点所示。Thread-0被park阻塞,如下图灰色节点所示:源码如下:下面是await()方法的整体流程,其中LockSupport.park(this)阻塞当前thread,后续唤醒会在这个程序点恢复执行。publicfinalvoidawait()throwsInterruptedException{//判断当前线程是否处于中断状态,如果是则直接抛出中断异常if(Thread.interrupted())thrownewInterruptedException();//将调用await的线程包装成一个Node,加入条件队列并返回Nodenode=addConditionWaiter();//完全释放节点持有的锁,因为其他线程唤醒当前线程的前提是【持有锁】intsavedState=fullyRelease(node);//设置开启中断模式为不中断,状态码为0intinterruptMode=0;//如果节点还没有转移到AQS阻塞队列,park阻塞等待进入阻塞队列while(!isOnSyncQueue(node)){//阻塞当前Thread,laterLockSupport.park(this);//如果被中断,则退出等待队列,相应的节点【也会被迁移到阻塞队列的尾部】,状态置0if((interruptMode=checkInterruptWhileWaiting(node))!=0)break;}//从逻辑上讲,这说明当前线程退出等待队列,进入【阻塞队列】//尝试枪锁,释放多少锁,然后【重新获取多少锁】,如果锁成功acquired,判断中断模式if(acquireQueued(node,savedState)&&interruptMode!=THROW_IE)interruptMode=REINTERRUPT;//节点在条件队列中,如果被外部线程打断,会被加入阻塞队列,但不会设置nextWaiter=nullif(node.nextWaiter!=null)//清理所有取消的条件在队列中节点unlinkCancelledWaiters();//条件为真,表示挂起时发生了中断if(interruptMode!=0)//应用中断模式reportInterruptAfterWait(interruptMode);},将线程封装成一个Node,添加到最后条件对象队列。此时,节点的等待状态为-2privateNodeaddConditionWaiter(){//获取当前条件队列尾节点的引用,保存在局部变量tNodet=lastWaiter;//当前队列不为空,节点状态不为CONDITION(-2),说明当前节点中断if(t!=null&&t.waitStatus!=Node.CONDITION){//清除条件队列中所有被取消的NodesunlinkCancelledWaiters();//清理并重新获取尾节点的引用t=lastWaiter;}//创建一个与当前线程关联的新节点,将状态设置为CONDITION(-2),并将其添加到队列的末尾Nodenode=newNode(Thread.currentThread(),Node.CONDITION);如果(t==null)firstWaiter=节点;//空队列直接放在队头【不用CAS,因为执行线程是持锁线程,并发安全】elset.nextWaiter=node;//添加到非空队列的尾部lastWaiter=node;//更新队列尾部引用返回节点;}清理条件队列中取消类型的节点,如中断,超时等,会导致节点转为取消//清理所有条件队列中已取消(非CONDITION)节点,【链表删除逻辑】privatevoidunlinkCancelledWaiters(){//从头节点开始遍历【先进先出】Nodet=firstWaiter;//指向正常的CONDITION节点Nodetrail=null;//等待队列为空while(t!=null){//获取当前节点后继节点Nodenext=t.nextWaiter;//判断t节点是否为CONDITION节点,如果不是condition队列中的CONDITION,则不正常if(t.waitStatus!=Node.CONDITION){//不是正常节点,t需要断开连接下一个节点t.nextWaiter=null;//如果条件为真,则遍历的节点没有遇到正常节点if(trail==null)//更新firstWaiter指针指向下一个节点firstWaiter=next;else//让上一个正常节点指向下一个当前取消节点Node,【删除异常节点】trail.nextWaiter=next;//t是尾节点,更新lastWaiter指向最后一个普通节点if(next==null)lastWaiter=trail;}else{//trail指向Normal节点trail=t;}//将t.next赋值给t,循环t=next;}}fullyRelease方法会让Thread-0释放锁,此时Thread-1会去竞争锁//Threads可能要重新进入,需要释放所有状态finalintfullyRelease(Nodenode){//完全释放锁是否成功,false表示成功booleanfailed=true;try{//获取当前线程持有的状态值总数intsavedState=getState();//release->tryRelease解锁重入锁if(release(savedState)){//释放成功failed=false;//返回解锁深度returnsavedState;}else{//如果解锁失败则抛出newIllegalMonitorStateException()thrownewIllegalMonitorStateException();}}finally{//失败释放成功,保存当前节点设置为取消状态if(failed)node.waitStatus=Node.CANCELLED;}}判断节点是否在AQS阻塞队列中,不在条件队列中finalbooleanisOnSyncQueue(Nodenode){//节点状态为CONDITION,signal方法是先修改状态再迁移,所以前驱节点为空,证明迁移还没有完成。如果(node.waitStatus==Node.CONDITION||node.prev==null)返回false;//表示当前节点已经成功进入Queue到阻塞队列,当前节点后面还有其他节点,因为条件队列的next指针为nullif(node.next!=null)returntrue;//说明【可能在阻塞队列中,但它是尾节点】//从阻塞队列的尾节点开始,向前移动【遍历寻找节点】,找到则返回true,找不到则返回falsereturnfindNodeFromTail(node);}signal过程Thread-1执行signal方法唤醒条件队列中的第一个节点,即Thread-0,条件队列为空的节点Thread-0的等待状态为改为0,重新加入到AQS队列的尾部,然后Thread-1释放锁,其他线程重新抢锁。源码如下:signal()方法是唤醒的入口方法publicfinalvoidsignal(){//判断调用signal方法的线程是否为独占锁持有线程if(!isHeldExclusively())thrownewIllegalMonitorStateException();//获取条件队列中的第一个节点Nodefirst=firstWaiter;//如果不为空,将第一个节点移到阻塞队列中if(first!=null)doSignal(first);}调用doSignal()方法唤醒节点第一个没有被取消到AQS队列尾部的节点]privatevoiddoSignal(Nodefirst){do{//建立表示当前节点的下一个节点为null,当前节点为尾节点,并且只有If((firstWaiter=first.nextWaiter)==null)lastWaiter=null;first.nextWaiter=null;//将等待队列中的Node转移到AQS队列中,如果不成功还有节点,继续循环}while(!transferForSignal(first)&&(first=firstWaiter)!=null);}//signalAll()会调用这个函数唤醒所有节点privatevoiddoSignalAll(Nodefirst){lastWaiter=firstWaiter=null;do{Nodenext=first.nextWaiter;first.nextWaiter=null;transferForSignal(第一);第一个=下一个;//唤醒所有节点并将它们放入阻塞队列}while(first!=null);}call使用transferForSignal()方法,先将节点的waitStatus改为0,然后加入AQS阻塞队列的尾部,将Thread-3的waitStatus改为-1//如果节点状态被取消,则返回false表示传输失败,否则传输为finalbooleantransferForSignal(Nodenode){//CAS修改当前节点状态为0,因为当前节点即将迁移到阻塞队列中//如果状态为no较长的CONDITION,表示线程被取消(await释放所有锁失败)或Interrupted(CancelAcquire可以中断)if(!compareAndSetWaitStatus(node,Node.CONDITION,0))//返回函数调用,继续搜索下一个节点返回false;//【先改变状态,再迁移】//将当前节点进入阻塞队列,p为当前节点在阻塞队列中的【前驱节点】Nodep=enq(node);intws=p.waitStatus;//如果前驱节点被取消或者不能设置状态为Node.SIGNAL,则unpark取消当前节点线程的阻塞状态,//让thread-0线程竞争锁,重新同步状态if(ws>0||!compareAndSetWaitStatus(p,ws,Node.SIGNAL))LockSupport.unpark(node.thread);returntrue;}总结本文讲解了ReentrantLock中条件变量的使用和原理,希望对大家有所帮助。