今天给各位分享T和P怎么摩擦下面教程的知识,其中也会对t和p怎么磨进行解释,如果能碰巧解决你现在面临的问题,别忘了关注本站,现在开始吧!
引言谈到并发编程,就不得不谈ReentrantLock,谈到ReentrantLock就会问实现原理,谈到原理就引出AQS(AbstractQueuedSynchronized),然后就被按在地上无情的摩擦。这篇文章主要讲解加锁过程,下一篇写释放锁过程。
ReentrantLock使用Locklock=newReentrantLock();nConditioncondition=lock.newCondition();nlock.lock();ntry{nwhile(条件判断表达式){ncondition.wait();n}n//处理逻辑n}finally{nlock.unlock();n}n复制代码
lock.lock()显示的获取锁,并在finally块中显示的释放锁,目的是保证在获取到锁之后,最终能够被释放。
lock()方法调用过程(默认非公平锁)非公平锁调用lock方法的源码如下:
staticfinalclassNonfairSyncextendsSync{ntprivatestaticfinallongserialVersionUID=7316153563782823691L;nntfinalvoidlock(){ntt//表示当前没有占有锁的线程,将锁的拥有者设置为当前线程nttif(compareAndSetState(0,1))ntttsetExclusiveOwnerThread(Thread.currentThread());ntt//获取锁nttelsentttacquire(1);nt}nntprotectedfinalbooleantryAcquire(intacquires){nttreturnnonfairTryAcquire(acquires);nt}n}n复制代码
下图是ReentrantLock.lock的调用过程图。
AQS加锁实现AQS(AbstractQueuedSynchronizer):是JDK提供的同步框架,内部维护了一个FIFO双向队列(即CLH同步队列)。通过此队列实现同步状态的管理(volatile修饰的state状态,用于标志是否持有锁)。
Node先了解AQS维护的队列节点结构,下面是队列节点Node的源码:
staticfinalclassNode{nt/**共享节点*/ntstaticfinalNodeSHARED=newNode();nnt/**独占节点*/ntstaticfinalNodeEXCLUSIVE=null;nnt/**因为超时或者中断,节点会被设置成取消状态,被取消的节点不会参与到竞争中,nt会一直是取消状态不会改变*/ntstaticfinalintCANCELLED=1;nnt/**后继节点处于等待状态,如果当前节点释放了同步状态或者被取消,nt会通知后继节点,使其得以运行*/ntstaticfinalintSIGNAL=-1;nnt/**节点在等待条件队列中,节点线程等待在condition上,当其他线程对conditionnt调用了signal后,该节点将会从等待队列中进入同步队列中,获取同步状态*/ntstaticfinalintCONDITION=-2;nnt/**nt*下一次共享式同步状态获取会无条件的传播下去nt*/ntstaticfinalintPROPAGATE=-3;nnt/**等待状态*/ntvolatileintwaitStatus;nnt/**前驱节点*/ntvolatileNodeprev;nnt/**后继节点*/ntvolatileNodenext;nnt/**获取同步状态的线程*/ntvolatileThreadthread;nnt/**nt*下一个条件队列等待节点nt*/ntNodenextWaiter;nntfinalbooleanisShared(){nttreturnnextWaiter==SHARED;nt}nntfinalNodepredecessor()throwsNullPointerException{nttNodep=prev;nttif(p==null)ntttthrownewNullPointerException();nttelsentttreturnp;nt}nntNode(){//UsedtoestablishinitialheadorSHAREDmarkernt}nntNode(Threadthread,Nodemode){//UsedbyaddWaiternttthis.nextWaiter=mode;nttthis.thread=thread;nt}nntNode(Threadthread,intwaitStatus){//UsedbyConditionnttthis.waitStatus=waitStatus;nttthis.thread=thread;nt}n}n复制代码FIFO队列(CLH队列)
队列用head,tail和state三个变量来维护,源码如下:
/**头节点*/nprivatetransientvolatileNodehead;nn/**尾节点*/nprivatetransientvolatileNodetail;nn/**同步状态*/nprivatevolatileintstate;n复制代码
结构示意图如下:
compareAndSetState调用首先尝试获取锁,调用compareAndSetState方法,期待值为0,新值为1。使用unsafe的compareAndSwapInt方法,通过一次CAS操作来修改state属性。
CAS操作即内存拿到volatile修饰的state属性值,与期望值0对比,如果取到的值为0,则执行+1操作,将state修改为1。其中还涉及知识点volatile修饰变量保证线程间可见,以及CAS操作的经典ABA问题。
源码如下:
/**n*如果当前状态值等于预期值,则自动将同步状态设置为给定的更新值。n*这个操作具有volatile读和写的内存语义。n*@paramexpect期望值n*@paramupdate新值n*@returnfalse返回表示实际值不等于预期值,true表示成功n*/nprotectedfinalbooleancompareAndSetState(intexpect,intupdate){nt//Seebelowforintrinsicssetuptosupportthisntreturnunsafe.compareAndSwapInt(this,stateOffset,expect,update);n}n复制代码
如果此方法执行成功,则调用setExclusiveOwnerThread方法将让线程占有锁,此时state已经置为1。
acquire调用进入此方法说明,当前已经有其他线程占有锁了。由于此种加锁方式是非公平锁,进入方法后,首先尝试获取锁,如果获取不到锁,那么再将当前线程置于队列中,让当前线程中断执行。
非公平锁在此方法中首先展示不公平,这种不公平是对在队列中的线程来说的。就像我们去银行办业务,如果我是VIP用户,我可以越过等待的用户先办理,这对于其他等待用户不公平。
publicfinalvoidacquire(intarg){ntif(!tryAcquire(arg)&&nttacquireQueued(addWaiter(Node.EXCLUSIVE),arg))nttselfInterrupt();n}n复制代码tryAcquire
调用此方法来尝试获取锁。源码如下:
finalbooleannonfairTryAcquire(intacquires){ntfinalThreadcurrent=Thread.currentThread();ntintc=getState();n/**首先获取state状态,此时第一个判断c==0后的操作是因为,n有可能在执行过程中,其他线程释放了锁,那么state为0,则直接让当前线程持有锁n*/ntif(c==0){nttif(compareAndSetState(0,acquires)){ntttsetExclusiveOwnerThread(current);ntttreturntrue;ntt}nt}n/**如果当前线程就是持有锁的线程,那么state+1,ntt此处提现了可重入锁的概念,每次线程重入该锁就重复此操作*/ntelseif(current==getExclusiveOwnerThread()){nttintnextc=c+acquires;nttif(nextc<0)//overflowntttthrownewError("Maximumlockcountexceeded");nttsetState(nextc);nttreturntrue;nt}ntreturnfalse;n}n复制代码
此处setState(nextc),只是单纯让state+1,而没有用CAS操作。
addWaiter负责把当前无法获得锁的线程包装为一个Node添加到队尾。
privateNodeaddWaiter(Nodemode){ntNodenode=newNode(Thread.currentThread(),mode);nt//Trythefastpathofenq;backuptofullenqonfailurentNodepred=tail;ntif(pred!=null){nttnode.prev=pred;nttif(compareAndSetTail(pred,node)){ntttpred.next=node;ntttreturnnode;ntt}nt}ntenq(node);ntreturnnode;n}n复制代码
其中参数mode是独占锁还是共享锁,默认为null,独占锁。追加到队尾的动作分两步:1.如果当前队尾已经存在(tail!=null),则使用CAS把当前线程追加到队尾。2.如果当前Tail为null或则线程调用CAS设置队尾失败,则通过enq方法继续追加。
enq通过for循环和CAS操作自旋过程,将当前线程加入队列中,源码如下:
privateNodeenq(finalNodenode){ntfor(;;){nttNodet=tail;n//如果队尾是null,则说明队列空了,将当前线程设置为头尾节点nttif(t==null){ntttif(compareAndSetHead(newNode()))ntttttail=head;ntt}else{n//队尾非空,通过CAS将当前线程加入队尾。ntttnode.prev=t;ntttif(compareAndSetTail(t,node)){nttttt.next=node;nttttreturnt;nttt}ntt}nt}n}n复制代码acquireQueued
此方法是对addWaiter的补充,将加入队列的线程中断执行,源码如下:
finalbooleanacquireQueued(finalNodenode,intarg){nt//操作失败标志ntbooleanfailed=true;nttry{ntt//线程中断标志nttbooleaninterrupted=false;nttfor(;;){nttt//当前节点的prev节点ntttfinalNodep=node.predecessor();nttt//如果前一节点是头结点,并且尝试获取同步状态成功ntttif(p==head&&tryAcquire(arg)){ntttt//将当前当前线程设置成头结点nttttsetHead(node);ntttt//将prev移除队列nttttp.next=null;//helpGCnttttfailed=false;nttttreturninterrupted;nttt}nttt//判断当前线程是否需要阻塞&&阻塞当前线程并且检验线程中断状态ntttif(shouldParkAfterFailedAcquire(p,node)&&nttttparkAndCheckInterrupt())nttttinterrupted=true;ntt}nt}finally{ntt//取消获取同步状态nttif(failed)ntttcancelAcquire(node);nt}n}n复制代码
p==head&&tryAcquire(arg),这里的判断也显示了非公平的意义。队里中有等待线程还要尝试获取锁。
shouldParkAfterFailedAcquire此方法是阻塞线程前最后的检查操作,通过prev节点的等待状态判断当前线程是否应该被阻塞,
privatestaticbooleanshouldParkAfterFailedAcquire(Nodepred,Nodenode){nt//拿到prev节点的等待状态ntintws=pred.waitStatus;ntntif(ws==Node.SIGNAL)ntt/*ntt*如果prev的status是signal,表示prev处于等待状态,可以阻塞当前线程,n*当prev释放了同步状态或者取消了,会通知当前节点。ntt*/nttreturntrue;ntif(ws>0){ntt/*ntt*status>0,表示为取消状态,需要将取消状态的节点从队列中移除ntt*直到找到一个状态不是取消的节点为止ntt*/nttdo{ntttnode.prev=pred=pred.prev;ntt}while(pred.waitStatus>0);nttpred.next=node;nt}else{ntt/*ntt*除了以上情况,通过CAS将prev的status设置成signalntt*/nttcompareAndSetWaitStatus(pred,ws,Node.SIGNAL);nt}ntreturnfalse;n}n复制代码parkAndCheckInterrupt
如果程序走到这个位置,那么就说明已经将当前线程加入队列中,可以让线程中断了。线程阻塞通过调用LockSupport.park()完成,而LockSupport.park()则调用sun.misc.Unsafe.park()本地方法,再进一步,HotSpot在Linux中中通过调用pthread_mutex_lock函数把线程交给系统内核进行阻塞。
privatefinalbooleanparkAndCheckInterrupt(){ntLockSupport.park(this);ntreturnThread.interrupted();n}n复制代码
至此加锁过程完成。
总结ReentrantLock加锁是通过AQS实现,AQS中维护了一个FIFO的队列,当存在锁竞争时构建队列,构建过程中使用CAS和自旋,保证线程能够进入队列。已经进入队列的线程需要阻塞,使用LockSupport.park()方法完成,阻塞线程能够让CPU更专注于执行持有锁的线程,而不是将资源浪费在尝试获取锁的自旋过程中。以上是对ReentrantLock加锁的过程分析,希望大佬多提意见。
链接:https://juejin.cn/post/7028106775555473445
文章到此结束,如果本次分享的T和P怎么摩擦下面教程和t和p怎么磨的问题解决了您的问题,那么我们由衷的感到高兴!