当前位置: 首页 > 后端技术 > Java

ReentrantLock的4个坑!

时间:2023-04-01 18:57:22 Java

在JDK1.5之前,synchronized的性能比较低,但是在JDK1.5中,正式推出了一个重量级的函数Lock,一举改变了Java中锁的格局。在JDK1.5之前,我们讲锁的时候只能使用内置的锁synchronized,而现在我们的锁实现多了一个显式锁Lock。synchronized我们在上一篇文章中介绍过,具体见下表:《synchronized 加锁 this 和 class 的区别!》《synchronized 优化手段之锁膨胀机制!》《synchronized 中的 4 个优化,你知道几个?》所以本文重点介绍Lock。Lock介绍Lock是一个顶层接口,它的所有方法如下图所示:它的子类列表如下:我们通常使用ReentrantLock来定义它的实例,它们之间的关系如下图所示:PS:Sync是同步锁的意思,FairSync是公平锁,NonfairSync是非公平锁。ReentrantLock的使用学任何技能都是从使用开始的,我们也不例外,先来看看ReentrantLock的基本使用:publicvoidmethod(){//锁操作lock.lock();try{//业务代码......}finally{//释放锁lock.unlock();ReentrantLock创建后,有两个关键操作:加锁操作:lock()释放锁操作:unlock()ReentrantLock中的陷阱1.ReentrantLock默认是非公平锁很多人(尤其是新手朋友)认为ReentrantLock的默认实现是公平锁,其实并不是这样,ReentrantLock默认是非公平锁(这个主要是出于性能考虑),比如下面的代码:importjava.util.concurrent.locks.ReentrantLock;publicclassLockExample{//创建一个锁对象privatestaticfinalReentrantLocklock=newReentrantLock();publicstaticvoidmain(String[]args){//定义线程任务Runnablerunnable=newRunnable(){@Overridepublicvoidrun(){//locklock.lock();try{//打印执行线程的名称System.out.println("线程:"+Thread.currentThread().getName());}finally{//释放锁lock.unlock();}}};//创建多个线程for(inti=0;i<10;i++){newThread(runnable).顺序递增,所以如果是公平锁,那么线程的执行应该是顺序递增的,但是从上面的结果可以看出线程的执行和打印是乱序的,这说明ReentrantLock是默认情况下是一个不公平的锁。设置ReentrantLock为公平锁也很简单。创建ReentrantLock时只需要设置一个true的构造参数,如下代码所示:importjava.util.concurrent.locks.ReentrantLock;publicclassLockExample{//创建锁对象(公平锁)privatestaticfinalReentrantLocklock=newReentrantLock(true);publicstaticvoidmain(String[]args){//定义线程任务Runnablerunnable=newRunnable(){@Overridepublicvoidrun(){//锁定lock.lock();try{//打印执行线程的名称System.out.println("Thread:"+Thread.currentThread().getName());}finally{//释放锁lock.unlock();}}};//创建多个线程for(inti=0;i<10;i++){newThread(runnable).start();}}}上面程序的执行结果如下:从上面的结果可以看出,当我们为ReentrantLock显式设置true构造参数后,ReentrantLock就变成了公平锁,线程获取锁的顺序也变得有序.其实从ReentrantLock的源码我们也可以看出是公平锁还是非公平锁。ReentrantLock部分源码实现如下:publicReentrantLock(){sync=newNonfairSync();}publicReentrantLock(booleanfair){sync=fair?newFairSync():newNonfairSync();}从上面源码可以看出,ReentrantLock默认会创建非公平锁。如果构造参数的值在创建时显式设置为true,就会被创建。一个公平的锁。2.在finally中释放锁在使用ReentrantLock的时候,一定要记得释放锁,否则锁会一直被占用,其他使用锁的线程会一直等待下去,所以我们在使用ReentrantLock的时候,一定要在finally释放锁,这样就可以保证锁一定会被释放。反例importjava.util.concurrent.locks.ReentrantLock;publicclassLockExample{//创建锁对象privatestaticfinalReentrantLocklock=newReentrantLock();publicstaticvoidmain(String[]args){//锁操作lock.锁();System.out.println("你好,ReentrantLock。");//这里会报异常,导致锁不能正常释放intnumber=1/0;//释放锁lock.unlock();系统。out.println("锁释放成功!");}}上面程序的执行结果如下:从上面的结果可以看出,发生异常时锁并没有正常释放,会导致其他使用锁的线程被永久锁住。在等待。正例importjava.util.concurrent.locks.ReentrantLock;publicclassLockExample{//创建一个锁对象privatestaticfinalReentrantLocklock=newReentrantLock();publicstaticvoidmain(String[]args){//锁操作lock.lock();尝试{System.out.println("Hello,ReentrantLock.");//这里会报异常intnumber=1/0;}finally{//释放锁lock.unlock();系统。out.println("锁释放成功!");}}}上面程序的执行结果如下:从上面的结果可以看出,虽然方法中出现异常情况,但是并不影响ReentrantLock锁的释放操作。这个锁的线程可以获取到并正常运行。3.锁无法释放。锁操作次数和解锁操作次数必须一一对应,不能多次释放一个锁,因为这样会导致程序报错。反例一次加锁对应两次解锁操作,导致程序报错终止执行。示例代码如下:importjava.util.concurrent.locks.ReentrantLock;publicclassLockExample{//创建锁对象privatestaticfinalReentrantLocklock=newReentrantLock();publicstaticvoidmain(String[]args){//锁操作lock.lock();//第一次释放锁try{System.out.println("执行业务1~");//业务代码1......}finally{//释放锁lock.unlock();System.out.println("锁释放锁");}//第二次释放锁try{System.out.println("执行业务2~");//业务代码2...}finally{//释放锁lock.unlock();System.out.println("锁释放锁");}//final打印操作System.out.println("程序执行完成。");}}上面程序的执行结果如下:从上面的结果可以看出,当执行到第二次unlock时,程序报错终止执行,导致None代码正常执行后出现异常.4.不要把锁放在try代码中。在使用ReentrantLock时,需要注意不要将加锁操作放在try代码中。这样会导致锁没有执行成功就释放了锁,导致程序执行异常。反例importjava.util.concurrent.locks.ReentrantLock;publicclassLockExample{//创建一个锁对象privatestaticfinalReentrantLocklock=newReentrantLock();publicstaticvoidmain(String[]args){try{//异常在这里intnum=1/0;//锁操作lock.lock();}finally{//释放锁lock.unlock();System.out.println("锁释放锁");}System.out.println("程序执行完毕。");}}上面程序的执行结果如下:从上面的结果可以看出,如果把加锁操作放在try代码中,可能会导致两个问题:加锁执行不成功,释放操作锁导致一个新的异常;释放锁的异常会覆盖掉程序原来的异常,从而增加排错的难度。小结本文介绍Java中显式锁Lock及其子类ReentrantLock的使用及注意事项。Lock在Java中占据了锁的半壁江山,但在使用时有4个问题需要注意:ReentrantLock默认是非公平锁,不是公平锁;加锁和释放锁的次数必须一致,否则会造成线程阻塞或程序异常;加锁操作一定要放在try代码之前,避免解锁成功释放锁异常;释放锁一定要放在finally里,否则线程会阻塞。本系列推荐4种创建线程的方法以及使用方法详解!Java中的用户线程和守护线程有这么大的区别吗?深入理解ThreadPool线程池的7种创建方式,强烈推荐大家使用……池化技术到底有多牛?看到线程和线程池的对比,惊呆了!并发中的线程同步和locksynchronized锁this和class的区别!volatile和synchronized的区别轻量级锁就一定比重量级锁快吗?这样终止线程会导致服务宕机?SimpleDateFormat线程不安全的5个解决方案!ThreadLocal不好用?这就是你没用的原因!ThreadLocal内存溢出代码演示及原因分析!信号量告白:限流器我用对了!CountDownLatch:别招手了,等大家再次加入我们!CyclicBarrier:当所有人都准备好后,司机就可以发动汽车了!synchronized优化的锁扩展机制意味着synchronized中的4个优化,你知道几个?关注公众号“Java中文社区”,查看更多有趣的Java并发知识文章。