【Java并发】揭秘Lock体系 -- 深入理解ReentrantLock
系列文章目录
文章目录
- 系列文章目录
- 1.重入锁的实现原理
- 2.公平锁与非公平锁
- 3.公平锁与非公平锁的比较
- 总结
ReentrantLock(重入锁)是实现 Lock 接口的一个类,也是在实际编程中使用频率很高的一个锁。ReentrantLock 最大的特点是支持重入性,能够对共享资源重复加锁,即当前线程获取锁资源后再次获取该锁不会被阻塞。同样,通过 synchronized 也可以隐式支持重入性,synchronized 通过持有锁的标志位表达锁资源的持有情况,线程获取锁资源后可通过自增以及自减的方式实现重入。与此同时,ReentrantLock 还支持公平锁和非公平锁两种方式。要想完全弄懂 ReentrantLock,需要理解两个要点:重入性的实现原理;公平锁和非公平锁。
1.重入锁的实现原理
要想实现重入性,需要解决两个问题:
- 在线程获取锁时,如果持有锁资源的线程是当前线程,则直接再次获取成功;
- 由于锁会被获取 n 次,那么只有锁在被释放同样的 n 次之后,该锁才算是完全释放成功。
如果需要实现一个同步组件,则通过 AQS 提供的模板方法实现同步语义就好。针对第一个问题,我们来看看 ReentrantLock 是怎样实现的。
以非公平锁为例,判断当前线程能否获得锁,核心方法为 nonfairTryAcquire (),源码如下:
final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();//1、如果该锁未被任何线程占有,则该锁能被当前线程获取if (c == 0) {if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}//2、若被占有,就会继续检查占有线程是否是当前线程else if (current == getExclusiveOwnerThread()) {// 3、再次获取,计数加1int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}
具体的代码逻辑可以查看注释,为了支持重入性,第 2 步增加了处理逻辑,如果该锁已经被线程占有了,会继续检查占有线程是否是当前线程。如果是,同步状态加 1 并返回 true,表示可以再次获取成功。每次重新获取都会对同步状态进行加 1 的操作。那么释放锁时的处理思路是怎样的呢?下面依然以公平锁为例,核心方法为 tryRelease (),源码如下:
protected final boolean tryRelease(int releases) {//1、同步状态减1int c = getState() - releases;if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;if (c == 0) {//2、只有当同步状态为0时,锁才算是成功被释放,返回truefree = true;setExclusiveOwnerThread(null);}// 3、锁未被完全释放,返回 falsesetState(c);return free;
}
具体的代码逻辑请看注释,需要注意的是,重入锁的释放必须等到同步状态为 0 时锁才算成功释放,否则就说明锁仍未被释放。如果锁被获取 n 次,释放了 n - 1 次,则该锁未被完全释放,返回 false,只有被释放 n 次才算完全释放,返回 true。理解了 ReentrantLock 重入性的实现原理,也就是理解了同步语义的第一条。
2.公平锁与非公平锁
ReentrantLock 支持两种锁:公平锁和非公平锁。“公平” 是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求上的绝对时间顺序,满足 FIFO。 ReentrantLock 的构造方法无参时是构造非公平锁,源码如下:
public ReentrantLock() {sync = new NonfairSync();
}
另外,ReentrantLock 还提供了一种方式,可传入一个布尔值,布尔值为 true 时是公平锁,为 false 时是非公平锁,源码如下:
public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();
}
在上面非公平锁获取时(nonfairTryAcquire () 方法),只是简单地获取了当前状态,做了一些逻辑处理,并没有考虑到当前同步队列中线程等待的情况。下面我们来看看公平锁的处理逻辑,其核心方法如下:
protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {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;
}
这段代码的逻辑与 nonfairTryAcquire 基本一致,唯一的不同在于增加了 hasQueuedPredecessors 的逻辑判断。通过方法名就可知道该方法是用来判断当前节点在同步队列中是否有前驱节点的,如果有前驱节点,说明有线程比当前线程更早请求资源,根据公平性,当前线程请求资源失败;如果没有前驱节点,才有做后面的逻辑判断的必要性。公平锁每次都是由同步队列中的第一个节点获取到锁,而非公平锁则不一定,有可能刚释放锁的线程下一次能继续获取到锁。
3.公平锁与非公平锁的比较
公平锁中,每次获取到锁的都是同步队列中的第一个节点,保证了请求资源在时间上的绝对顺序;而非公平锁中,有可能刚释放锁的线程下一次能继续获取到该锁,导致有的线程可能永远也无法获取到锁,造成 “饥饿” 现象。
公平锁为了保证时间上的绝对顺序,需要频繁地进行上下文切换;而非公平锁会降低一定的上下文切换,降低性能开销。因此,ReentrantLock 默认选择非公平锁,就是为了减少一部分上下文切换,保证了系统更大的吞吐量。
总结
本文带大家深入理解ReentrantLock,通过对ReentrantLock源码的阅读,学习ReentrantLock可重入性的实现原理和公平锁、非公平锁的实现机制。
以上就是本文全部内容,感谢各位能够看到最后,如有问题,欢迎各位大佬在评论区指正,希望大家可以有所收获!创作不易,希望大家多多支持!