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

SimpleReentrantLock(可重入锁)

时间:2023-03-12 00:15:42 科技观察

1.前言在Java5.0之前,可以用来协调访问共享对象的机制只有synchronized和volatile。Java5.0增加了一个新的机制:ReentrantLock。与前面提到的机制相反,ReentrantLock不是一种替代内置锁定的方法,而是在内置解锁机制不适用时作为可选的高级功能。2.简介ReentrantLock实现了Lock和java.io.Serializable接口,提供了和synchronized一样的互斥和内存可见性。ReentrantLock提供可重入锁定语义,可以为共享资源重复。加锁意味着当前线程不会被阻塞再次获取锁,而且相对于synchronized,在处理锁不可用时也提供了更高的灵活性。同时ReentrantLock也支持公平锁和非公平锁。锁定两种方式。ReentrantLock类层次结构:ReentrantLock实现了Lock和Serializable接口。一共有三个内部类。Sync、NonfairSync、FairSyncSync是继承AbstractQueuedSynchronizer的抽象类型。这个AbstractQueuedSynchronizer是一个模板类,实现了很多锁相关的功能。它还提供了钩子方法供用户实现,例如tryAcquire、tryRelease等。Sync实现了AbstractQueuedSynchronizer的tryRelease方法。NonfairSync和FairSync两个类继承自Sync并实现了lock方法。公平抢占和不公平抢占分别对tryAcquire有不同的实现。3.可重入锁可重入锁,又称递归锁,从名字上理解,字面意思就是可重入锁。可重入是指任何线程在获取锁后都可以再次获取锁而不会被阻塞。锁阻塞,首先他需要满足两个条件:1)线程再次获取锁:需要判断获取锁的线程是否是当前占用锁的线程,如果是则获取成功2)最后一次释放锁:线程重复n次获取锁,然后第n次释放锁后,其他线程才能获取到锁。最终释放锁需要将锁计为获取。该计数表示当前线程被重复获取的次数。当它被释放时,计数递减。当计数为0时,表示已经成功释放锁。3.1锁的实现使用ReentrantLockCase:Locklock=newReentrantLock();lock.lock();try{//更新对象状态//捕获异常,必要时恢复不变条件}catch(Exceptione){e.printStackTrace();}finally{lock.unlock();}以上代码是使用Lock接口的标准方式。这种形式比使用内置锁(synchronized)更复杂,必须在finally块中释放锁。否则,如果被保护代码中抛出异常,锁永远不会被释放。4、ReentrantLock源码分析在介绍中我们知道ReentrantLock继承自Lock接口。Lock提供了一些获取和释放锁的方法,以及获取条件判断的方法。锁是通过实现它来控制的,因为它是一个显示锁,所以需要显示指定的起始位置和结束位置。这里介绍一下Lock接口的方法:ReentrantLock同样实现了上述接口的内容。同时ReentrantLock提供了公平锁和非公平锁两种模式。如果没有特别指定使用哪种方法,那么ReentrantLock默认就是非公平锁。首先我们看ReentrantLock的构造函数:/***无参构造函数*/publicReentrantLock(){sync=newNonfairSync();}/***有参数构造函数*参数为Boolean*/publicReentrantLock(booleanfair){sync=fair?newFairSync():newNonfairSync();}从上面的源码我们可以看出:ReentrantLock更倾向于使用无参构造函数,即非公平锁,但是当我们调用带参数的构造函数时,我们可以指定用于操作的锁(公平锁或非公平锁)。参数是布尔类型。如果指定为false,则表示非公平锁。如果指定为true,则表示公平锁Sync类是ReentrantLock的自定义同步组件。它是ReentrantLock中的一个内部类,继承自AQS(AbstractQueuedSynchronizer)。Sync有两个子类:公平锁FairSync和非公平锁NonfairSyncReentrantLock,锁的获取和释放操作委托给同步组件。下面看一下非公平锁的lock()方法:4.1NonfairSync.lock()1.NonfairSync.lock()方法流程图:2.lock方法详解在初始化ReentrantLock的时候,如果不通过Referto,使用默认构造函数,那么默认使用非公平锁,也就是NonfairSync2)当我们调用ReentrantLock的lock()方法时,实际上调用的是NonfairSync的lock()方法,代码如下:staticfinalclassNonfairSyncextendsSync{privatestaticfinallongserialVersionUID=7316153563782823691L;/***Performslock.Tryimmediatebarge,backinguptonormal*acquireonfailure.*/finalvoidlock(){//该方法首先使用CAS操作尝试抢占锁//快速尝试将状态从0设置为1,如果状态=0表示当前没有线程获取到锁。if(compareAndSetState(0,1))//state置1,表示获取锁成功//如果成功,则将当前线程设置在这个锁上,表示抢占成功,重新进入加锁时,setExclusiveOwnerThread(Thread.currentThread());else//如果失败,调用AbstractQueuedSynchronizer.acquire()模板方法,等待抢占。acquire(1);}protectedfinalbooleantryAcquire(intacquires){returnnonfairTryAcquire(acquires);}}acquire(1)的调用实际上使用了AbstractQueuedSynchronizer的acquire()方法,它是一个锁抢占的模板,acquire()代码为比较简单:publicfinalvoidacquire(intarg){//先尝试获取锁,如果没有,则将当前线程的一个节点添加到CLH队列中,表示等待抢占。//然后进入CLH队列的抢占模式,进入时也会进行一次锁获取操作。如果还是获取不到,//调用LockSupport.park()挂起当前线程。那么当前线程什么时候会被唤醒呢?当//持有锁的线程调用unlock()时,它将唤醒CLH队列头节点的下一个节点上的线程//并调用LockSupport.unpark()方法。if(!tryAcquire(arg)&&acquireQueued(addWaiter(Node.EXCLUSIVE),arg))selfInterrupt();}acquire()会先调用tryAcquire()钩子方法尝试获取锁。该方法在NonfairSync.tryAcquire()下面的nonfairTryAcquire(),源码如下://一个尝试跳入队列的过程finalbooleannonfairTryAcquire(intacquires){finalThreadcurrent=Thread.currentThread();//获取statevalueintc=getState();//比较锁的状态是否为0,如果为0则当前没有线程获取锁if(c==0){//尝试原子抢占锁(设置state为1,然后设置当前线程为独占线程)if(compareAndSetState(0,acquires)){//设置独占锁成功setExclusiveOwnerThread(current);returntrue;}}//如果当前锁的状态不是0state!=0,比较当前线程和占用锁的线程,看是否是线程elseif(current==getExclusiveOwnerThread()){//如果是,增加state变量的值,从这里我们可以看出可重入锁是可重入的,因为同一个线程可以重复使用它占用的锁intnextc=c+acquires;//太多次重入,大于Integer.MAXif(nextc<0)//overflowthrownewError("Maximumlockcountexceeded");setState(nextc);returntrue;}//以上两个条件不满足则返回失败falsereturnfalse;}一次tryAcquire()返回false,就会进入acquireQueued()流程,即基于CLH队列的抢占模式。在CLH锁队列的末尾添加一个等待节点。该节点保存当前线程,通过调用addWaiter()实现。这里需要考虑初始化情况。当第一个等待节点进入时,需要初始化一个头节点,然后放入当前节点加入尾部,后续节点直接加入尾部代码如下://AbstractQueuedSynchronizer.addWaiter()privateNodeaddWaiter(Nodemode){//初始化一个节点保存当前线程Nodenode=newNode(Thread.currentThread(),mode);//当CLH队列不为空时,看情况,直接在队尾插入节点nodepred=tail;if(pred!=null){node.prev=pred;//如果pred还在队尾(即没有被其他线程更新)),然后更新tai??l到node节点(即当前线程快速设置到队列的尾部)if(compareAndSetTail(pred,node)){pred.next=node;returnnode;}}//当CLH队列为空,调用enq方法初始化队列enq(node);returnnode;}privateNodeenq(finalNodenode){//循环不断尝试将node节点插入队列尾部for(;;){Nodet=tail;if(t==null){//初始化节点,head和tail都指向一个空节点if(compareAndSetHead(newNode()))tail=head;}??else{node.prev=t;if(compareAndSetTail(t,node)){t.next=node;returnt;}}}}会点头后e加入到CLH队列中,进入acquireQueued()方法finalbooleanacquireQueued(finalNodenode,intarg){booleanfailed=true;try{booleaninterrupted=false;//循环等待前驱节点执行完毕for(;;){finalNodep=node.predecessor();if(p==head&&tryAcquire(arg)){//通过tryAcquire获取锁,如果获取到锁,说明头节点释放了锁setHead(node);//Setthecurrentnodeastheheadnodep.next=null;//helpGC//设置上一个节点的next变量为null,在下次GC时清理。failed=false;//标记失败设置为falsereturninterrupted;}//中断if(shouldParkAfterFailedAcquire(p,node)&&//是否需要阻塞parkAndCheckInterrupt())//阻塞,返回线程interrupted=true;}}finally{if(failed)cancelAcquire(node);}}如果尝试获取锁失败,会进入shouldParkAfterFailedAcquire()方法,判断当前线程是否阻塞/***确保当前节点的前驱节点状态为SIGNAL*SIGNAL表示线程Wakeup阻塞线程后释放锁*只有能被唤醒,当前线程才能安全阻塞*/privatestaticbooleanshouldParkAfterFailedAcquire(Nodepred,Nodenode){intws=pred.waitStatus;if(ws==Node.SIGNAL)//如果前驱节点状态为SIGNAL//说明当前线程需要阻塞,因为前节点promise执行完毕后,会通知当前节点唤醒returntrue;if(ws>0){//ws>0表示prev节点已经取消prev;//Continuously从队列中移除前驱节点被取消的节点}while(pred.waitStatus>0);pred.next=node;}else{//初始化状态,设置前驱节点的状态为SIGNALcompareAndSetWaitStatus(pred,ws,Node.SIGNAL);}returnfalse;}进入阻塞阶段,会进入parkAndCheckInterrupt()方法,会调用LockSupport.park(this)挂起当前线程。代码如下://从方法名可以看出这个方法做了两件事true,否则returnfalse//可能在挂起阶段被中断returnThread.interrupted();}4.2非公平锁NonfairSync.unlock()2.1unlock()方法示意图2.1unlock()方法详解1)调用unlock()方法,其实就是直接调用AbstractQueuedSynchronizer.release()操作。2)进入release()方法,先在内部尝试tryRelease()操作,主要是去除锁的独占线程,然后状态减一。当状态变为0时,表示锁被完全释放。3)如果tryRelease成功,则将CHL队列的头节点状态设置为0,然后唤醒下一个非取消节点线程。4)一旦下一个节点的线程被唤醒,被唤醒的线程就会进入acquireQueued()代码流程去获取锁。代码如下:publicvoidunlock(){sync.release(1);}publicfinalbooleanrelease(intarg){//尝试将当前锁的锁计数(状态)值减1,if(tryRelease(arg)){Nodeh=head;if(h!=null&&h.waitStatus!=0)//waitStatus!=0表示要么处于CANCEL状态,要么SIGNAL表示下一个线程正在等待它唤醒。也就是说,如果waitStatus不为零,则表示其后继者正在等待唤醒。unparkSuccessor(h);//成功返回truereturntrue;}//否则返回falsereturnfalse;}privatevoidunparkSuccessor(Nodenode){intws=node.waitStatus;//如果waitStatus<0,则将当前节点清零(ws<0)compareAndSetWaitStatus(node,ws,0);//如果后面的节点为空或者已经被取消,则从队列的末尾找到第一个waitStatus<=0的节点,也就是没有被取消的节点取消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);}当然在release()方法中,不仅仅是设置state-1这么简单,-1之后也需要处理。如果-1之后的新状态=0,说明当前锁已经被线程释放,会唤醒线程等待队列中的下一个线程。protectedfinalbooleantryRelease(intreleases){intc=getState()-releases;//判断当前线程是否在调用,而不是抛出IllegalMonitorStateExceptionif(Thread.currentThread()!=getExclusiveOwnerThread())thrownewIllegalMonitorStateException();booleanfree=false;//c==0,释放锁,将当前持有的线程设置为nullif(c==0){free=true;setExclusiveOwnerThread(null);}//setstatesetState(c);returnfree;}privatevoidunparkSuccessor(Nodenode){intws=node.waitStatus;if(ws<0)compareAndSetWaitStatus(node,ws,0);Nodes=node.next;if(s==null||s.waitStatus>0){s=null;//来自回到前面,找到最靠近头部的节点,并且waitStatus<=0//其实在ReentrantLock中,waitStatus应该只有0和-1,需要被唤醒的都是-1(Node.SIGNAL)for(Nodet=tail;t!=null&&t!=node;t=t.prev)if(t.waitStatus<=0)s=t;}if(s!=null)LockSupport.unpark(s.thread);//唤醒并挂起启动线程}重点:unlock最好放在finally,因为如果你不要用finally来释放Lock,相当于启动了一颗定时炸弹。如果发生错误,我们很难追溯原来的错误位置,因为没有记录应该在什么地方,什么时候释放锁,这也是为什么ReentrantLock不能完全替代synchronized,因为锁并不会在什么时候自动清空程序执行控制离开受保护的代码块。4.3FairLockFairSyncFairSync比较简单,只是重写的两个方法与NonfairSync不同finalvoidlock(){acquire(1);}protectedfinalbooleantryAcquire(intacquires){finalThreadcurrent=Thread.currentThread();intc=getState();if(c==0){if(!hasQueuedPredecessors()&&//无前置节点compareAndSetState(0,acquires)){//无锁setExclusiveOwnerThread(current);returntrue;}}elseif(current==getExclusiveOwnerThread()){intnextc=c+acquires;if(nextc<0)thrownewError("Maximumlockcountexceeded");setState(nextc);returntrue;}returnfalse;}五、公平锁和非公平锁的区别锁的公平性是相对于In获取锁的顺序条款。如果是公平锁,那么锁的获取顺序应该符合请求的绝对时间顺序,即先进先出。线程获取锁的顺序与调用锁的顺序相同,可以保证老线程排队使用锁,新线程仍然排队使用。锁。非公平锁只要CAS设置同步状态成功,就说明当前线程已经获取到了锁。线程获取锁的顺序与调用锁的顺序无关。排队线程上的锁。ReentrantLock默认使用非公平锁是基于性能的考虑。为了保证线程正确排队,公平锁需要增加阻塞和唤醒的时间开销。如果直接跳入队列获取非公平锁,跳过队列的处理,速度会更快。6、ReenTrantLock(可重入锁)和synchronized的区别6.1ReentrancyReenTrantLock(可重入锁)字面意思是可重入锁。synchronized关键字使用的锁也是可重入的。这个没有太大区别。两者都是同一个线程,一次都没有进入过,锁计数器加1,所以直到锁计数器降为0后才能释放锁。6.2lockSynchronized的实现依赖于JVM的实现,而ReenTrantLock是由JDK实现。有什么不同?说白了,类似于操作系统控制实现和用户自己代码实现的区别。前者的实现比较难看,后者有直接的源码可以阅读。6.3性能差异在Synchronized优化之前,synchronized的性能比ReenTrantLock差很多,但是自从Synchronized引入了偏向锁和轻量级锁(自旋锁)后,两者性能相差无几。在两种方法都具备的情况下,官方更推荐使用synchronized。其实我感觉synchronized的优化是借鉴了ReenTrantLock中的CAS技术。他们都是想办法解决用户态的锁问题,避免阻塞线程进入内核态。6.4功能差异的方便性:很明显Synchronized的使用更方便简洁,编译器保证加锁和释放锁,而ReenTrantLock需要手动声明加锁和释放锁。为了避免忘记手动释放锁造成死锁,所以最好在finally中声明释放锁。细粒度灵活的锁:显然ReenTrantLock比Synchronized好,但是ReenTrantLock没有办法完全替代SynchronizedReenTrantLock的独特能力1)ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓公平锁就是先等待的线程先获取锁。2)ReenTrantLock提供了一个Condition(条件)类,用于唤醒需要分组唤醒的线程,而不是像synchronized那样随机唤醒一个线程或者唤醒所有线程。3)ReenTrantLock提供了一种可以中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。7.总结ReentrantLock是java中一个非常重要的并发工具。与javanativesynchronized相比,具有更好的性能。学习ReentrantLock,我们主要是要了解它,公平锁和非公平锁的实现,以及可重入锁的实现获取和释放的过程,还有最重要的是要了解AQS(AbstractQueuedSynchronizer),这是基础用于实现可重入锁。ReentrantLock是一个比较轻量级的锁,使用面向对象的思想来实现锁功能,比原来的synchronized关键字更容易理解。