开发者

一文带你掌握Java ReentrantLock加解锁原理

目录
  • 简要总结 ReentrantLock
  • ReentrantLock 结构分析
  • lock 加锁过程
    • 阻塞前
    • park方法被唤醒后
  • unlock 释放锁过程
    • 对比 Synchronized

      简要总结 ReentrantLock

      实现原理:volatile 变量 + CAS设置值 + AQS + 两个队列

      实现阻塞:同步队列 + CAS抢占标记为 valatile 的 state

      实现等待唤醒:await :持有锁,park ->加入等待队列 ;signal:唤醒下一个等待队列节点,转移进入同步队列,然后CAS抢占或者按照阻塞队列等待抢占。接着 await 后续内容程序得以继续执行。

      ReentrantLock 结构分析

      ReentrantLock 继承了Lock接口, lock方法实际上是调用了Sync的子类NonfairSync(非公平锁)的lock方法。ReentrantLock的真正实现在他的两个内部类NonfairSync 和 FairSync中,默认实现是非公平锁。并且内部类都继承于内部类Sync,而Sync根本的实现则是大名鼎鼎的 AbstractQueuedSynchronizer 同步器(AQS)。

      一文带你掌握Java ReentrantLock加解锁原理

      具体详见如下代码:

      publicclassReentrantLockimplementsLock,Java.io.Serializable{
      privatestaticfinallongserialVersionUID=7373984872572414699L;
      /**Synchronizerprovidingallimplementationmechanics*/
      privatefinalSyncsync;
      
      publicReentrantLock(){
      sync=newNonfairSync();
      }
      abstractstaticclassSyncextendsAbstractQueuedSynchronizer{
      ……省略代码
      }
      
      //非公平锁
      staticfinalclassNonfairSyncextendsSync{
      privatestaticfinallongserialVersionUID=7316153563782823691L;
      
      /**
      *Performslock.Tryimmediatebarge,backinguptonormal
      *acquireonfailure.
      */
      finalvoidlock(){
      if(compareAndSetState(0,1))
      setExclusiveOwnerThread(Thread.currentThread());
      else
      acquire(1);
      }
      
      protectedfinalbooleantryAcquire(intacquires){
      returnnonfairTryAcquire(acquires);
      }
      }
      //公平锁
      staticfinalclassFairSyncextendsSync{
      privatestaticfinallongserialVersionUID=-3000897897090466540L;
      
      finalvoidlock(){
      acquirehttp://www.devze.com(1);
      }
      
      /**
      *FairversionoftryAcquire.Don'tgrantAccessunless
      *recursivecallornowaitersorisfirst.
      */
      protectedfinalbooleantryAcquire(intacquires){
      ……省略
      }
      }
      //lock方法本质就是调用sync类
      publicvoidlock(){
      sync.lock();
      }
      }
      

      lock 加锁过程

      按照调用 lock 方法是否抢占锁成功,可以以调用 park 方法为界限,将加锁的过程分为两部分:一部分是当前线程被阻塞前,另一部分是线程被唤醒继续执行后。(这里以非公平锁为例)

      阻塞前

      1.直接通过CAS尝试获取锁,设置state为1。如果获取成功则将锁标识设为独占,就是是将当前线程设置给 exclusiveOwnerThread。

      finalvoidlock(){
      if(compareAndSetState(0,1))
      setExclusiveOwnerThread(Thread.currentThread());
      else
      acquire(1);
      }
      

      2.如果获取失败,再次尝试获取,调用acquire。

      3.tryAcquire ->:判断锁是否被占有,如果空闲则再次尝试CAS获取锁;如果已被占有则对比占有锁的线程是否为本线程,是的话将state+1,这就是可重入锁的关键逻辑。

      //AbstractQueuedSynchronizer
      publicfinalvoidacquire(intarg){
      if(!tryAcquire(arg)&&
      acquireQueued(addwaiter(Node.EXCLUSIVE),arg))
      selfInterrupt();
      }
      //ReentrantLock.NonfairSync
      protectedfinalbooleantryAcquire(intacquires){
      returnnonfairTryAcquire(acquires);
      }
      //ReentrantLock.Sync
      finalbooleannonfairTryAcquire(intacquires){
      finalThreadcurrent=Thread.currentThread();
      intc=getState();
      if(c==0){
      //cas再次尝试获取
      if(compareAndSetState(0,acquires)){
      setExclusiveOwnerThread(current);
      returntrue;
      }
      }
      elseif(current==getExclusiveOwnerThread()){
      //可重入逻辑
      intnextc=c+acquires;
      if(nextc<0)//overflow
      thrownewError("Maximumlockcountexceeded");
      setState(nextc);
      returntrue;
      }
      returnfalse;
      }
      

      4.如果获取失败则将节点插入队列尾部,如果队列为空,则会初始化队列,并且设置头尾节点为空节点,再将Node设为尾节点。

      //获取锁失败
      acquireQueued(addWaiter(Node.EXCLUSIVE),arg))
      
      //加入同步队列
      privateNodeaddWaiter(Nodemode){
      Nodenode=newNode(Thread.currentThread(),mode);
      //Trythefastpathofenq;backuptofullenqonfailure
      Nodepred=tail;
      if(pred!=null){
      node.prev=pred;
      //通过CAS设置尾节点为当前节点,前驱节点为之前的尾节点。
      if(compareAndSetTail(pred,node)){
      pred.next=node;
      returnnode;
      }
      }
      //如果当前链表为空,则在此处进行初始化
      enq(node);
      returnnode;
      }
      privateNodeenq(finalNodenode){
      for(;;){
      Nodet=tail;
      if(t==null){//Mustinitialize
      if(compareAndSetHead(newNode()))
      tail=head;
      }else{
      //追加到队列尾
      node.prev=t;
      if(compareAndSetTail(t,node)){
      t.next=node;
      returnt;
      }
      }
      }
      }
      

      5.将新建的Node传入acquireQueued,获取前驱节点,如果节点就是head 头节点,那么尝试CAS竞争锁(head随时释放)。如果抢占成功将头节点设为自己。

      finalbooleanacquireQueued(finalNodenode,intarg){
      booleanfailed=true;
      try{
      booleaninterrupted=false;
      for(;;){
      finalNodep=node.predecessor();
      //如果是头节点,再次尝试
      if(p==head&&tryAcquire(arg)){
      setHead(node);
      p.next=null;//helpGC
      failed=false;
      returninterrupted;
      }
      if(shouldParkAfterFailedAcquire(p,node)&&
      parkAndCheckInterrupt())
      interrupted=true;
      }
      }finally{
      if(failed)
      cancelAcquire(node);
      }
      }
      

      6.如果没有抢占成功,则进入shouldParkAfterFailedAcquire逻辑,将前驱节点设置为Signal,表示后继节点(也就是当前节点)需要前驱节点去唤醒。设置完之后再次进入自旋锁,尝试获得锁。

      关于Node的状态这里说明一下:

      节点刚创建的时候,status=0,假设这时候本节点就是head节点,那么他会进入else逻辑,将自身状态设置为Signal,然后再次进入自旋,尝试获取锁。如果还是没有获取到锁,那么再次进入shouldParkAfterFailedAcquire方法后会进入第一个if逻辑,方法返回True。

      /**
      *Checksandupdatesstatusforanodethatfailedtoacquire.
      *Returnstrueifthreads编程客栈houldblock.Thisisthemainsignal
      *controlinallacquireloops.Requiresthatpred==node.prev.
      *如果获取锁失败,检查并且更新节点。如果需要被park阻塞,返回true。
      *在所有的循环逻辑中,这是主要的信号控制逻辑。
      *
      * pred:表示前驱节点
      * node:表示当前线程节点
      */
      privatestaticbooleanshouldParkAfterFailedAcquire(Nodepred,Nodenode){
      intws=pred.waitStatus;
      if(ws==Node.SIGNAL)
      //第二次尝试获取锁会进入这段逻辑
      /*
      *Thisnodehasalreadysetstatusaskingarelease
      *tosignalit,soitcansafelypark.
      */
      //表明线程已经准备好被阻塞并等待之后被唤醒
      returntrue;
      if(ws>0){
      /*
      *Predecessorwascancelled.Skipoverpredecessorsand
      *indicateretry.
      */
      //若pred.waitStatus状态位大于0,说明这个前驱点已经取消了获取锁的操作,
      //doWhile循环会递归删除掉这些放弃获取锁的节点
      do{
      node.prev=pred=pred.prev;
      }while(pred.waitStatus>0);
      pred.next=no编程de;
      }else{
      /*
      *节点刚创建的时候,status=0,逻辑会走到这里将自身状态设置为signal
      *waitStatusmustbe0orPROPAGATE.Indicatethatwe
      *needasignal,butdon'tparkyet.Callerwillneedto
      *retrytomakesureitcannotacquirebeforeparking.
      */
      //若状态位不为Node.SIGNAL,且没有取消操作,则会尝试将前驱节点状态位修改为Node.SIGNAL
      //表示将会唤醒后继节点
      compareAndSetWaitStatus(pred,ws,Node.SIGNAL);
      }
      returnfalse;
      }
      

      7.第二次自旋获取失败后开发者_C培训,由于前驱节点已经是Signal,这时进入parkAndCheckInterrupt,将当前线程阻塞,等待被唤醒。后续其他线程如果也尝试抢占锁,会同样被阻塞。

      privatefinalbooleanparkAndCheckInterrupt(){
      //阻塞线程
      LockSupport.park(this);
      //线程继续执行
      returnThread.interrupted();
      }
      

      park方法被唤醒后

      在其他线程释放锁资源后,唤醒下一个节点,park的后半部分逻辑继续执行。

      1.继续执行之前Park之后的逻辑,在此处线程被唤醒。这里会返回中断标记,这也是为什么ReentrantLock可以相应中断的原因。

      一文带你掌握Java ReentrantLock加解锁原理

      2.然后再次进入自旋锁,使用CAS获取到锁标记,将头节点设为当前节点,然后返回中断标记跳出循环。

      3.至此,获取锁流程结束。

      unlock 释放锁过程

      1.尝试释放锁,用state减去1,判断是否等于0。如果等于0表示已经完全释放锁,将线程标记设为null。否则释放失败,表示当前线程仍在继续持有,继续持有说明有重入情况。

      //ReentrantLock
      publicvoidunlock(){
      sync.release(1);
      }
      //AQS
      publicfinalbooleanrelease(intarg){
      //释放锁
      if(tryRelease(arg)){
      Nodeh=head;
      if(h!=null&&h.waitStatus!=0)
      //唤醒后继节点
      unparkSuccessor(h);
      returntrue;
      }
      returnfalse;
      }
      //释放锁
      protectedfinalbooleantryRelease(intreleases){
      intc=getState()-releases;
      if(Thread.currentThread()!=getExclusiveOwnerThread())
      thrownewIllegalMonitorStateException();
      booleanfree=false;
      if(c==0){
      free=true;
      //释放锁
      setExclusiveOwnerThread(null);
      }
      setState(c);
      returnfree;
      }
      

      2.拿到头节点,然后解锁后继节点。如果当前节点状态小于0(signal=-1),则修改节点status为0。然后向后递归找到status小于等于0的节点(正常为0),调用unpark解除阻塞。返回解锁成功。

      //唤醒后继节点
      privatevoidunparkSuccessor(Nodenode){
      /*
      *Ifstatusisnegative(i.e.,possiblyneedingsignal)try
      *toclearinanticipationofsignalling.ItisOKifthis
      *failsorifstatusischangedbywaitingthread.
      */
      intws=node.waitStatus;
      if(ws<0)
      compareAndSetWaitStatus(node,ws,0);
      
      /*
      *Threadtounparkisheldinsuccessjavascriptor,whichisnormally
      *justthenextnode.Butifcancelledorapparentlynull,
      *traversebackwardsfromtailtofindtheactual
      *non-cancell编程edsuccessor.
      */
      //拿到下一个节点
      Nodes=node.next;
      //要解除阻塞的线程在后继节点中,通常只是下一个节点。但如果取消或明显为空,则从尾部向前遍历以找到实际未取消的继任者。
      if(s==null||s.waitStatus>0){
      s=null;
      for(Nodet=tail;t!=null&&t!=node;t=t.prev)
      if(t.waitStatus<=0)
      s=t;
      }
      if(s!=null)
      //解锁
      LockSupport.unpark(s.thread);
      }
      

      一文带你掌握Java ReentrantLock加解锁原理

      3.在这之后便继续开始执行之前被阻塞的线程中的逻辑。

      到这里 ReentrantLock 的加解锁过程原理便讲解结束,关于条件队列的内容,有兴趣后续文章会做讲解。

      对比 Synchronized

      既然已经了解了 ReentrantLock ,那么在此对大家所熟知的 Synchronized 进行一个对比。

      与Synchronized相同点

      1.ReentrantLock和synchronized都是独占锁,只允许线程互斥的访问临界区。

      但是实现上两者不同:synchronized加锁解锁的过程是隐式的,用户不用手动操作,优点是操作简单,但显得不够灵活。一般并发场景使用synchronized的就够了;ReentrantLock需要手动加锁和解锁,且解锁的操作尽量要放在finally代码块中,保证线程正确释放锁。ReentrantLock操作较为复杂,但是因为可以手动控制加锁和解锁过程,在复杂的并发场景中能派上用场。

      2.ReentrantLock和synchronized都是可重入锁。

      synchronized因为可重入因此可以放在被递归执行的方法上,且不用担心线程最后能否正确释放锁;而ReentrantLock在重入时要却确保重复获取锁的次数必须和重复释放锁的次数一样,否则可能导致其他线程无法获得该锁。

      3.都可以实现线程之间的等待通知机制。使用synchronized结合Object上的wait和notify方法可以实现线程间的等待通知机制。ReentrantLock结合Condition接口同样可以实现这个功能。而且相比前者使用起来更清晰也更简单。

      与Synchronized 不同点

      • ReentrantLock是Java层面的实现,synchronized是JVM层面的实现。
      • 使用synchronized关键字实现同步,线程执行完同步代码块会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),而ReentrantLock需要手动释放锁需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁。
      • synchronized是非公平锁,ReentrantLock可以实现公平和非公平锁。
      • ReentrantLock 可以设置超时获取锁。在指定的截止时间之前获取锁,如果截止时间到了还没有获取到锁,则返回。配合重试机制更好的解决死锁。
      • ReentrantLock上等待获取锁的线程是可中断的,线程可以放弃等待锁。而synchonized会无限期等待下去。
      • ReentrantLock 的 tryLock() 方法可以尝试非阻塞的获取锁,调用该方法后立刻返回,如果能够获取则返回true,否则返回false。
      • synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁,并且可以主动尝试去获取锁。

      到此这篇关于一文带你掌握Java ReentrantLock加解锁原理的文章就介绍到这了,更多相关Java ReentrantLock加解锁内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

      0

      上一篇:

      下一篇:

      精彩评论

      暂无评论...
      验证码 换一张
      取 消

      最新开发

      开发排行榜