Java并发编程之ReentrantLock实现原理及源码剖析
前面《Java并发编程之JUC并发核心AQS同步队列原理剖析》介绍了AQS的同步等待队列的实现原理及源码分析,这节我们将介绍一下基于AQS实现的ReentranLock的应用、特性、实现原理及源码分析。
一、ReentrantLock简介
ReentrantLock位于Java的juc包里面,从JDK1.5开始出现,是基于AQS同步队列的独占模式实现的一种锁。ReentrantLock使用起来比synchronized更加灵活,可以自己控制加锁、解锁的逻辑。ReentrantLock跟synchronized一样也是可重入的锁,提供了公平/非公平两种模式:
- 公平锁:多个线程竞争锁的时候,会先判断等待队列中是否有等待的线程节点,如果有则当前线程会进行排队,锁的获取顺序符合请求的绝对时间顺序,也就是 FIFO
- 非公平锁:当前线程竞争锁的时候不管有没有其他线程节点在排队,都会先通过CAS尝试获取锁,获取失败了才会进行排队。
通过new ReentrantLock()的方式创建的是非公平锁,要想创建公平锁需要在构造方法中指定new ReentrantLock(true)。ReentrantLock的常用方法如下:
- void lock() 获取锁,如果当前线程获取锁成功将返回,获取锁失败线程将被阻塞、挂起
- void lockInterruptibly() throws InterruptedException 可中断的获取锁,和lock方法的不同之处在于该方法会响应中断,即在锁的获取过程中可以中断当前线程
- boolean tryLock() 尝试非阻塞的获取锁,方法会立即返回,获取锁成功返回true,否则返回false
- boolean tryLock(long time, TimeUnit unit) throws InterruptedException 尝试在指定超时时间内获取锁,如果当前线程获取了锁会立即返回true,如果被其他线程获取了锁则会被阻塞挂起,该方法会在下面三种情况下返回:1,在超时时间内获取了锁,返回true;2,在超时时间内线程被中断;3,超时时间结束,返回false。
- void unlock() 释放锁
- Condition newCondition() 获取等待通知组件,该组件与当前的锁绑定,当前线程只有获取了锁,才能调用Condition的wait()方法,调用wait()方法后会释放锁
二、ReentrantLock使用
ReentrantLock的使用方式一般如下,一定要在finally里面进行解锁,防止程序出现异常无法解锁
ReentrantLock lock = new ReentrantLock(); lock.lock(); try { System.out.println("获取了锁"); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); }
下面通过一个程序示例,演示一下ReentrantLock的使用:对同一个lock对象做多次加锁,解锁,演示一下ReentrantLock的锁重入
public class ReentrantLockTest { private Integer counter = 0; private ReentrantLock lock = new ReentrantLock(); public void modifyResources(String threadName){ System.out.println("线程:--->"+threadName+"等待获取锁"); lock.lock(); System.out.println("线程:--->"+threadName+"第一次加锁"); counter++; System.out.println("线程:"+threadName+"做第"+counter+"件事"); //重入该锁,我还有一件事情要做,没做完之前不能把锁资源让出去 lock.lock(); System.out.println("线程:--->"+threadName+"第二次加锁"); counter++; System.out.println("线程:"+threadName+"做第"+counter+"件事"); lock.unlock(); System.out.println("线程:"+threadName+"释放一个锁"); lock.unlock(); System.out.println("线程:"+threadName+"释放一个锁"); } public static void main(String[] args) throws InterruptedException { ReentrantLockTest tp = new ReentrantLockTest(); new Thread(()->{ String threadName = Thread.currentThread().getName(); tp.modifyResources(threadName); },"Thread:张三").start(); new Thread(()->{ String threadName = Thread.currentThread().getName(); tp.modifyResources(threadName); },"Thread:李四").start(); Thread.sleep(100); } }
程序运行输出如下所示:上面代码中lock加锁两次然后解锁两次,在张三线程两次解锁完成之前,李四线程一直在等待。ReentrantLock加锁了几次,就要解锁相同的次数才可以释放锁。
线程:--->Thread:张三等待获取锁
线程:--->Thread:张三第一次加锁
线程:Thread:张三做第1件事
线程:--->Thread:张三第二次加锁
线程:--->Thread:李四等待获取锁
线程:Thread:张三做第2件事
线程:Thread:张三释放一个锁
线程:Thread:张三释放一个锁
线程:--->Thread:李四第一次加锁
线程:Thread:李四做第3件事
线程:--->Thread:李四第二次加锁
线程:Thread:李四做第4件事
线程:Thread:李四释放一个锁
线程:Thread:李四释放一个锁
三、ReentrantLock源码分析
ReentrantLock实现了Lock接口,它有一个内部类Sync实现了前面介绍过的AbstractQueuedSynchronizer,而其公平锁、非公平锁分别通过Sync的子类FairSync、NonFairSync(也是ReentrantLock的内部类)实现。下面看下其UML图
lock()方法调用时序图如下:
前面《Java并发编程之JUC并发核心AQS同步队列原理剖析》介绍AQS的时候说过,AbstractQueuedSynchronizer中有一个状态变量state,在ReentrantLock中state等于0表示没有线程获取锁,如果等于1说明有线程获取了锁,如果大于1说明获取锁的线程加锁的次数,加了几次锁就必须解锁几次,每次unlock解锁state都会减1,减到0时释放锁。
1、非公平锁源码分析
前面一篇博客《Java并发编程之JUC并发核心AQS同步队列原理剖析》对AQS介绍的已经非常详细了,所以下面源码分析中牵涉AQS中的方法就不再进行介绍了,想了解的话可以看下那篇博客。
先看下非公平锁的加锁lock方法,lock方法中调用了sync的lock方法,而sync对象时根据构造ReentrantLock时是公平锁(FairSync)还是非公平锁(NonFairSync)。
public void lock() { sync.lock(); }
这里调用的是非公平锁,所以我们看下 NonFairSync的lock方法:进来时不管有没有其他线程持有锁或者等待锁,会先调用AQS中的compareAndSetState方法尝试获取锁,如果获取失败,会调用AQS中的acquire方法
final void lock() { /** * 第一步:直接尝试加锁 * 与公平锁实现的加锁行为一个最大的区别在于,此处不会去判断同步队列(CLH队列)中 * 是否有排队等待加锁的节点,上来直接加锁(判断state是否为0,CAS修改state为1) * ,并将独占锁持有者 exclusiveOwnerThread 属性指向当前线程 * 如果当前有人占用锁,再尝试去加一次锁 */ if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else //AQS定义的方法,加锁 acquire(1); }
下面看下acquire方法,会先调用NonFairSync类中重写的tryAcquire方法尝试获取锁,如果获取锁失败会调用AQS中的acquireQueued方法进行排队、阻塞等处理。
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
下面看下NonFairSync类中重写的tryAcquire方法,里面又调用了nonfairTryAcquire方法
protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); }
下面看下nonfairTryAcquire方法:
- 判断state如果为0,通过CAS的方式尝试获取锁,如果获取锁成功,则将当前线程设置为独占线程
- 如果state不为0,则判断当前线程是否跟独占线程时同一个线程,如果是同一个线程则将锁的state加1,也就是锁的重入次数加1
- 否则获取锁失败,返回false
final boolean nonfairTryAcquire(int acquires) { //acquires = 1 final Thread current = Thread.currentThread(); int c = getState(); /** * 不需要判断同步队列(CLH)中是否有排队等待线程 * 判断state状态是否为0,不为0可以加锁 */ if (c == 0) { //unsafe操作,cas修改state状态 if (compareAndSetState(0, acquires)) { //独占状态锁持有者指向当前线程 setExclusiveOwnerThread(current); return true; } } /** * state状态不为0,判断锁持有者是否是当前线程, * 如果是当前线程持有 则state+1 */ else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } //加锁失败 return false; }
下面看下非公平锁的解锁过程:unlock方法中调用了AQS中的release方法
public void unlock() { sync.release(1); }
AQS中的release方法如下所示:会先调用AQS的子类Sync中重写的tryRelease方法去释放锁,如果是否锁成功,则唤醒同步队列中head的后续节点,后续节点线程被唤醒会去竞争锁。
public final boolean release(int arg) { if (tryRelease(arg)) {//释放一次锁 Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h);//唤醒后继结点 return true; } return false; }
Sync中重写的tryRelease方法:
获取当前的state值,然后减1
判断当前线程是否是锁的持有线程,如果不是会抛出异常。
如果state的值被减到了0,表示锁已经被释放,会将独占线程设置为空null,将state设置为0,返回true,否则返回false。
/** * 释放锁 */ protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; }
2、公平锁源码分析
先看下公平锁的加锁lock方法,lock方法中调用了sync的lock方法,这里调用的是FairSync的lock方法。
public void lock() { sync.lock(); }
FairSync的lock方法直接调用了AQS中的acquire方法,没有像非公平锁先通过CAS的方式先去尝试获取锁
final void lock() { acquire(1); }
下面看下acquire方法,会先调用FairSync类中重写的tryAcquire方法尝试获取锁,如果获取锁失败会调用AQS中的acquireQueued方法进行排队、阻塞等处理。
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
下面看下FairSync类中重写的tryAcquire方法,这个方法跟NonFairSync的唯一区别就是state为0的时候,公平锁会先通过hasQueuedPredecessors()方法判断是否队列中是否有等待的节点,如果没有才会尝试通过CAS的方式去获取锁,非公平锁不会判断直接回尝试获取锁。
protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { /** * 与非公平锁中的区别,需要先判断队列当中是否有等待的节点 * 如果没有则可以尝试CAS获取锁 */ if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { //独占线程指向当前线程 setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
公平锁的unlock方法与非公平锁的代码一样,这里就不再介绍了。
到此这篇关于Java并发编程之ReentrantLock实现原理及源码剖析的文章就介绍到这了,更多相关Java ReentrantLock内容请搜索猪先飞以前的文章或继续浏览下面的相关文章希望大家以后多多支持猪先飞!
相关文章
- 这篇文章主要介绍了如何利用java语言实现经典《复杂迷宫》游戏,文中采用了swing技术进行了界面化处理,感兴趣的小伙伴可以动手试一试...2022-02-01
java 运行报错has been compiled by a more recent version of the Java Runtime
java 运行报错has been compiled by a more recent version of the Java Runtime (class file version 54.0)...2021-04-01- 这篇文章主要介绍了在java中获取List集合中最大的日期时间操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-08-15
- 这篇文章主要介绍了教你怎么用Java获取国家法定节假日,文中有非常详细的代码示例,对正在学习java的小伙伴们有非常好的帮助,需要的朋友可以参考下...2021-04-23
- 这篇文章主要介绍了Java如何发起http请求的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-03-31
- 说起C#和Java这两门语言(语法,数据类型 等),个人以为,大概有90%以上的相似,甚至可以认为几乎一样。但是在工作中,我也发现了一些细微的差别...2020-06-25
- 这篇文章主要介绍了解决Java处理HTTP请求超时的问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-03-29
- 这篇文章主要介绍了java 判断两个时间段是否重叠的案例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-08-15
- 这篇文章主要介绍了超简洁java实现双色球若干注随机号码生成(实例代码),本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2021-04-02
- 这篇文章主要介绍了Java生成随机姓名、性别和年龄的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-10-01
java 画pdf用itext调整表格宽度、自定义各个列宽的方法
这篇文章主要介绍了java 画pdf用itext调整表格宽度、自定义各个列宽的方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-01-31- 这篇文章主要介绍了java正则表达式判断前端参数修改表中另一个字段的值,需要的朋友可以参考下...2021-05-07
Java使用ScriptEngine动态执行代码(附Java几种动态执行代码比较)
这篇文章主要介绍了Java使用ScriptEngine动态执行代码,并且分享Java几种动态执行代码比较,需要的朋友可以参考下...2021-04-15- 这篇文章主要介绍了Java开发实现人机猜拳游戏,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2020-08-03
- 这篇文章主要介绍了Java List集合返回值去掉中括号('[ ]')的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-08-29
Java中lombok的@Builder注解的解析与简单使用详解
这篇文章主要介绍了Java中lombok的@Builder注解的解析与简单使用,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2021-01-06- 下面小编就为大家带来一篇java中String类型变量的赋值问题介绍。小编觉得挺不错的。现在分享给大家,给大家一个参考。...2016-03-28
Java 8 Stream 的终极技巧——Collectors 功能与操作方法详解
这篇文章主要介绍了Java 8 Stream Collectors 功能与操作方法,结合实例形式详细分析了Java 8 Stream Collectors 功能、操作方法及相关注意事项,需要的朋友可以参考下...2020-05-20- 这篇文章主要介绍了Java线程池中的各个参数如何合理设置操作,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教...2021-06-19
- 在Java中,我们可以利用多线程来最大化地压榨CPU多核计算的能力,下面这篇文章主要给大家介绍了关于java中多线程与线程池基本使用的相关资料,需要的朋友可以参考下...2021-09-13