首页>>后端>>java->ReentrantLock(重入锁)详述

ReentrantLock(重入锁)详述

时间:2023-12-01 本站 点击:0

简介

重入锁ReentrantLock指的是支持同一个线程对资源的重复加锁。ReentrantLock中有公平锁和非公平锁的两种实现。

synchronized

synchronized关键字支持隐式的重入;当一个线程获取到锁时,是支持这个线程多次获取这个锁的,不会出现自己阻塞自己的情况,并且我们开发过程中对于synchronized关键字也不需要关心锁的释放。举个递归的例子我们来看synchronized关键字对锁的重入。

代码示例

packagecom.lizba.p6;/***<p>*synchronized锁重入测试*</p>**@Author:Liziba*@Date:2021/6/2121:45*/publicclassSynchronizedTest{publicstaticvoidmain(String[]args){intsum=cal(0);System.out.println(sum);}/***简单递归重入,递归十次*@parami*@return*/privatestaticsynchronizedintcal(inti){if(i<10){returncal(++i);}returni;}}

输出结果为10,在这个过程中main线程重复进入synchronized关键字修饰的cal(int i)方法。因此也证明了上面的说法正确。

ReentrantLock

ReentrantLock基于AQS和Lock来实现的,那如果是我们ReentrantLock要实现可重入,需要解决和实现如下两个问题:

同一个线程多次获取锁,则需要判断当前来获取锁的线程和占有锁的线程是否为同一个线程

多次获取锁,则需要多次释放这个锁,可以通过一个计数器累加和自减来记录锁的重复获取与释放

ReentrantLock中有两种重入锁的实现,分别是:

NonfairSync-非公平锁

FairSync-公平锁

公平锁和非公平锁的本质区别就在于,获取锁的顺序是否符合FIFO,对于公平锁来说先加入同步队列等待的线程,必将会先获取到同步状态(锁),对于非公平锁来说,获取到锁的顺序不确定。

简单使用

在进行源码分析之前,先来看看ReentrantLock是如何使用的,ReentrantLock的使用非常简单,示例代码如下:

packagecom.lizba.p6;importjava.util.concurrent.locks.Lock;importjava.util.concurrent.locks.ReentrantLock;/***<p>*ReentrantLock使用示例代码*</p>**@Author:Liziba*@Date:2021/6/2122:09*/publicclassReentrantLockDemo{/** 初始化一个非公平锁,ReentrantLock的默认实现是非公平锁 */privatestaticLocklock=newReentrantLock();publicstaticvoidmain(String[]args){newThread(()->testReentrantLock(),"ThreadA").start();newThread(()->testReentrantLock(),"ThreadB").start();}/***假设为获取锁执行的相关业务逻辑方法*/privatestaticvoidtestReentrantLock(){//获取锁要在try值外,如果获取锁过程中异常,不会无故释放锁lock.lock();try{System.out.println(Thread.currentThread().getName()+":获取了锁");Thread.sleep(1000);}catch(InterruptedExceptione){e.printStackTrace();}finally{//释放锁在finally代码块中lock.unlock();System.out.println(Thread.currentThread().getName()+":释放了锁");}}}

如上简单使用的案例,Thread A和Thread B输出的结果如下(这里是非公平锁不要被现在的顺序迷惑):

Sync—ReentrantLock组合的自定义同步器抽象

/***ReentrantLock内部类Sync,也是其内部组合实现的自定义同步器的抽象*/abstractstaticclassSyncextendsAbstractQueuedSynchronizer{privatestaticfinallongserialVersionUID=-5179523762034025860L;/***定义获取锁的抽象方法,由NonfairSync和FairSync去实现各自获取锁的方式*/abstractvoidlock();/***NonfairSync中tryAcquire调用的方法*/finalbooleannonfairTryAcquire(intacquires){finalThreadcurrent=Thread.currentThread();intc=getState();//如果当前共享状态未被其他线程占用if(c==0){//尝试通过CAS占有当前共享状态if(compareAndSetState(0,acquires)){//设置共享状态持有线程为当前线程setExclusiveOwnerThread(current);returntrue;}}//如果共享状态已被占用,则判断当前占用共享状态的线程是否就是当前线程elseif(current==getExclusiveOwnerThread()){//如果是则自增获取次数,设值stateintnextc=c+acquires;if(nextc<0)thrownewError("Maximumlockcountexceeded");setState(nextc);returntrue;}returnfalse;}/***释放共享状态*/protectedfinalbooleantryRelease(intreleases){//计算减少后的值stateintc=getState()-releases;//判断当前线程和持有共享状态的线程是否是同一个线程,不是则抛出异常if(Thread.currentThread()!=getExclusiveOwnerThread())thrownewIllegalMonitorStateException();booleanfree=false;//如果state递减后的值为0了,表示线程释放完共享状态,需要情况持有共享状态的线程变量if(c==0){free=true;setExclusiveOwnerThread(null);}//设置statesetState(c);returnfree;}/***判断占用共享状态的线程是否是当前线程*/protectedfinalbooleanisHeldExclusively(){returngetExclusiveOwnerThread()==Thread.currentThread();}/***如果state不为0,则获取共享状态的持有线程,否则返回null*/finalThreadgetOwner(){returngetState()==0?null:getExclusiveOwnerThread();}/***如果当前线程持有共享状态,则返回state,否则返回0*/finalintgetHoldCount(){returnisHeldExclusively()?getState():0;}/***判断当前共享状态是否被持有*/finalbooleanisLocked(){returngetState()!=0;}privatevoidreadObject(java.io.ObjectInputStreams)throwsjava.io.IOException,ClassNotFoundException{s.defaultReadObject();setState(0);//resettounlockedstate}finalConditionObjectnewCondition(){returnnewConditionObject();}}

NonfairSync源码分析

/***非公平锁的代码实现*/staticfinalclassNonfairSyncextendsSync{privatestaticfinallongserialVersionUID=7316153563782823691L;/****/finalvoidlock(){//CAS设置共享状态,返回true表示成功获取共享状态if(compareAndSetState(0,1))//设置当前线程为共享状态的持有线程setExclusiveOwnerThread(Thread.currentThread());else//否则调用AQS中的acquire(intarg)尝试获取同步状态,失败则加入等待队列,自旋获取共享状态acquire(1);}/***该方法在调用Sync中定义的nonfairTryAcquire方法,上面详细讲述了*主要是做线程重入判断,并对state共享状态值的增加(当获取同步状态的线程是持有同步状态的线程也就是所说的重入)*/protectedfinalbooleantryAcquire(intacquires){returnnonfairTryAcquire(acquires);}}

NonfairSync的tryRelease调用的是Sync中的tryRelease,上面在Sync源码中详细介绍了。假设线程A对共享状态tryAcquire(1)了十次,那么线程A在调用tryRelease(1)的前9次,state的值依次递减的同时一定会返回false,只有第十次也就是最后一调用tryRelease(1),同步状态才会真正的释放,方法返回true,持有共享状态的线程置为null。

FairSync源码分析

/***公平锁的代码实现*/staticfinalclassFairSyncextendsSync{privatestaticfinallongserialVersionUID=-3000897897090466540L;/***调用AQS中的acquire(intarg)尝试获取同步状态,失败则加入等待队列,自旋获取共享状态*/finalvoidlock(){acquire(1);}/***公平锁和非公平锁的主要区别在于此方法*/protectedfinalbooleantryAcquire(intacquires){//获取到当前线程finalThreadcurrent=Thread.currentThread();//获取当前同步状态intc=getState();//如果同步状态为0,则说明当前同步状态已完全释放if(c==0){//1、hasQueuedPredecessors判断当前节点是否存在前驱节点//2、如果不存在则CAS设置state的值if(!hasQueuedPredecessors()&&compareAndSetState(0,acquires)){//前两个都满足则,设置同步状态持有的线程为当前线程setExclusiveOwnerThread(current);returntrue;}}//否则判断当前线程和持有共享状态的线程是否是同一个线程elseif(current==getExclusiveOwnerThread()){//如果是,重入,状态值增加intnextc=c+acquires;if(nextc<0)thrownewError("Maximumlockcountexceeded");//设值新的状态值setState(nextc);returntrue;}returnfalse;}}

FairSync能够顺序的获取共享状态,也就是保证加入同步队列的顺序和获取到同步状态的顺序一致,依靠的是hasQueuedPredecessors()这个判断当前节点是否存在前驱节点的判断。

NonfairSync和FairSync区别示例代码

packagecom.lizba.p6;importjava.util.ArrayList;importjava.util.Collection;importjava.util.Collections;importjava.util.List;importjava.util.concurrent.TimeUnit;importjava.util.concurrent.locks.Lock;importjava.util.concurrent.locks.ReentrantLock;importjava.util.stream.Collectors;/***<p>*公平和非公平锁测试*</p>**@Author:Liziba*@Date:2021/6/2123:33*/publicclassFairAndUnfairTest{/**定义公平锁*/privatestaticLockfairLock=newReentrantLockCustomize(true);/**定义非公平锁*/privatestaticLockunfairLock=newReentrantLockCustomize(false);/***测试公平锁和非公平锁*@paramlock*/privatestaticvoidtestFairAndUnfairLock(Locklock){for(inti=1;i<=5;i++){newJob(lock,""+i).start();}}/***定义线程实现,打印当前线程和等待队列中的线程*/privatestaticclassJobextendsThread{privateLocklock;publicJob(Locklock,Stringname){this.lock=lock;setName(name);}@Overridepublicvoidrun(){//通过两次输出,来判断是否与队列中一致for(inti=0;i<2;i++){lock.lock();try{System.out.println("获取锁的线程:"+Thread.currentThread().getName());System.out.println("同步队列中的线程:"+((ReentrantLockCustomize)lock).getQueuedThreads().stream().map(t->t.getName()).collect(Collectors.joining(",")));TimeUnit.SECONDS.sleep(1);}catch(InterruptedExceptione){e.printStackTrace();}finally{lock.unlock();}}}}/***自定义可重入锁,主要新增getQueuedThreads()方法,用于获取等待队列中的线程*/privatestaticclassReentrantLockCustomizeextendsReentrantLock{publicReentrantLockCustomize(booleanfair){super(fair);}/***返回正在等待获取锁的线程列表,获取的实现列表逆序输出,反转后则为FIFO队列的原本顺序**@return等待队列中的线程顺序集合*/publicCollection<Thread>getQueuedThreads(){List<Thread>ts=newArrayList<>(super.getQueuedThreads());Collections.reverse(ts);returnts;}}}

测试公平锁

//测试公平锁testFairAndUnfairLock(fairLock);

查看输出

同步队列中等待的线程的顺序为2、3、4、5此时输出的结果为1、2、3、4、5和 1、2、3、4、5,按照同步队列中等待的顺序顺序输出,先进入同步队列的先获取到锁。

测试非公平锁

//测试非公平锁testFairAndUnfairLock(unfairLock);

查看输出

同步队列中等待的线程顺序为2、4、5、3当时线程1却连续获取了两次锁,因此非公平锁是不能保证获取锁的顺序的。

存在问题:

非公平锁很明显存在线程“饥饿”问题,也就是一个线程获取到锁后会继续的再次获取到锁的可能性比较大,导致其他线程等待时间较长,那么为何ReentrantLock还有继续设置其为默认实现呢?这个主要原因是,公平锁会带来大量的线程切换的开销,而非公平锁虽然可能会导致线程“饥饿”问题,但是其吞吐量是远远大于公平锁的,相比之下非公平锁优势更大。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:/java/5485.html