您现在的位置是:亿华云 > 知识
面试官问了一个离奇的关于ReentrantLock的问题,我对答如流
亿华云2025-10-04 03:11:26【知识】2人已围观
简介来源:码农本农先了解一下读本篇前,一定要确保已经读过本公众号的AQS讲解。我们知道实现一把锁要有如下几个逻辑锁的标识线程抢锁的逻辑线程挂起的逻辑线程存储逻辑线程释放锁的逻辑线程唤醒的逻辑我们在讲解AQ
来源:码农本农
先了解一下
读本篇前,面试一定要确保已经读过本公众号的官问个离关于AQS讲解。
我们知道实现一把锁要有如下几个逻辑
锁的的对答标识线程抢锁的逻辑线程挂起的逻辑线程存储逻辑线程释放锁的逻辑线程唤醒的逻辑我们在讲解AQS的时候说过AQS基本负责了实现锁的全部逻辑,唯独线程抢锁和线程释放锁的问题逻辑是交给子类来实现了,而ReentrantLock作为最常用的面试独占锁,其内部就是官问个离关于包含了AQS的子类实现了线程抢锁和释放锁的逻辑。
我们在使用ReentrantLock的的对答时候一般只会使用如下方法
ReentrantLock lock=new ReentrantLock();
lock.lock();
lock.unlock();
lock.tryLock();
Condition condition=lock.newCondition();
condition.await();
condition.signal();
condition.signalAll();
技术架构
如果我们自己来实现一个锁,那么如何设计呢?问题
根据AQS的逻辑,我们写一个子类sync,面试这个类一定会调用父类的acquire方法进行上锁,同时重写tryAcquire方法实现自己抢锁逻辑,官问个离关于也一定会调用release方法进行解锁,的对答同时重写tryRelease方法实现释放锁逻辑。问题

那么ReentrantLock是怎么实现的香港云服务器呢?
ReentrantLock的实现的类架构如下,ReentrantLock对外提供作为一把锁应该具备的官问个离关于api,比如lock加锁,的对答unlock解锁等等,而它内部真正的实现是通过静态内部类sync实现,sync是AQS的子类,是真正的锁,因为这把锁需要支持公平和非公平的特性,所以sync又有两个子类FairSync和NonfairSync分别实现公平锁和非公平锁。

因为是否公平说的是抢锁的时候是否公平,那两个子类就要在上锁方法acquire的调用和抢锁方法tryAcquire的重写上做文章。
公平锁做了什么文章?
static final class FairSync extends Sync {
final void lock() {
acquire(1);
}
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;
}
}
公平锁比较简单,直接调用了父级类AQS的acquire方法,因为AQS的站群服务器锁默认就是公平的排队策略。
重写tryAcquire方法的逻辑为:
判断当前锁是否被占用,即state是否为0如果当前锁没有被占用,然后会判断等待队列中是否有线程在阻塞等待,如果有,那就终止抢锁,如果没有,就通过cas抢锁,抢到锁返回true,没有抢到锁返回false。如果当前锁已经被占用,然后判断占用锁的线程是不是自己,如果是,就会将state加1,表示重入,返回true。如果不是自己那就是代表没有抢到锁,返回false。公平就公平在老老实实排队。
非公平锁做了什么文章?
static final class NonfairSync extends Sync {
final void lock() {
if(compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
elseacquire(1);
}
protected final boolean tryAcquire(int acquires) {
returnnonfairTryAcquire(acquires);
}
}
//nonfairTryAcquire代码在父类sync里面
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if(c == 0) {
if(compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
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;
}
非公平锁也很简单,没有直接调用了父级类AQS的acquire方法,而是源码下载先通过cas抢锁,它不管等待队列中有没有其他线程在排队,直接抢锁,这就体现了不公平。
它重写tryAcquire方法的逻辑为:
判断当前锁是否被占用,即state是否为0如果当前锁没有被占用,就直接通过cas抢锁(不管等待队列中有没有线程在排队),抢到锁返回true,没有抢到锁返回false。如果当前锁已经被占用,然后判断占用锁的线程是不是自己,如果是,就会将state加1,表示重入,返回true。如果不是自己那就是代表没有抢到锁,返回false。公平锁和非公平分别重写了tryAcquire方法,来满足公平和非公平的特性。那么tryAcquire方法也是需要子类重写的,因为它和是否公平无关,因此tryAcquire方法被抽象到sync类中重写。
sync类中
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);
returnfree;
}
释放锁的逻辑如下:
获取state的值,然后减1如果state为0,代表锁已经释放,清空aqs中的持有锁的线程字段的值如果state不为0,说明当前线程重入了,还需要再次释放锁将state写回释放锁往往和抢锁逻辑是对应的,每个子类抢锁逻辑不同的话,释放锁的逻辑也会对应不同。
具体实现
接下来我们通过ReentrantLock的使用看下它的源码实现
class X {
private final ReentrantLock lock = new ReentrantLock();
Condition condition1=lock.newCondition();
Condition condition2=lock.newCondition();
public void m() {
lock.lock();
try {
if(条件1){
condition1.await();
}
if(条件2){
condition2.await();
}
} catch (InterruptedException e) {
} finally {
condition1.signal();
condition2.signal();
lock.unlock();
}
}
}
先看这个方法:lock.lock()
ReentrantLock类
public void lock() {
sync.lock();
}
NonfairSync 类中
final void lock() {
if(compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
elseacquire(1);
}
FairSync 类中
final void lock() {
acquire(1);
}
公平锁和非公平锁中都实现了lock方法,公平锁直接调用AQS的acquire,而非公平锁先抢锁,抢不到锁再调用AQS的acquire方法进行上锁
进入acquire方法后的逻辑我们就都知道了。
再看这个方法lock.unlock()
public void unlock() {
sync.release(1);
}
unlock方法内直接调用了AQS的Release方法进行解锁的逻辑,进入release方法后逻辑我们都已经知道了,这里不再往下跟。
最后看这个方法lock.tryLock()
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
returnsync.tryAcquireNanos(1, unit.toNanos(timeout));
}
tryLock方法直接调用sync的tryAcquireNanos方法,看过AQS的应该知道tryAcquireNanos这个方法是父类AQS的方法,这个方法和AQS中的四个核心方法中的Acquire方法一样都是上锁的方法,无非是上锁的那几个步骤,调用tryAcquire方法尝试抢锁,抢不到锁就会进入doAcquireNanos方法。
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if(Thread.interrupted())
throw new InterruptedException();
returntryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
doAcquireNanos这个方法做的其实就是入队,阻塞等一系列上锁操作,逻辑和Acquire方法中差不多,但是有两点不同:
该方法支持阻塞指定时长。该方法支持中断抛异常。看下下面的代码
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if(nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for(;;) {
final Node p = node.predecessor();
if(p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // helpGC
failed = false;
return true;
}
nanosTimeout = deadline - System.nanoTime();
if(nanosTimeout <= 0L)
return false;
if(shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if(Thread.interrupted())
throw new InterruptedException();
}
} finally {
if(failed)
cancelAcquire(node);
}
}
这里的阻塞不再是LockSupport类的park方法,而是parkNanos方法,这个方法支持指定时长的阻塞,AQS正是利用这个方法实现阻塞指定时长,当自动唤醒后,循环中会判断是否超过设定时长,如果超过直接返回false跳出循环。
在阻塞期间,如果线程被中断,就会抛出异常,同样会跳出循环,外面可以通过捕获这个异常达到中断阻塞的目的。
可见ReentrantLock其实啥也没做,其tryLock方法完全是依赖AQS实现。
lock.newCondition();
在AQS那篇我们说过Condition是AQS中的条件队列,可以按条件将一批线程由不可唤醒变为可唤醒。
ReentrantLock类
public Condition newCondition() {
returnsync.newCondition();
}
sync静态内部类
final ConditionObject newCondition() {
returnnew ConditionObject();
}
sync提供了创建Condition对象的方法,意味着ReentrantLock也拥有Condition的能力。
ReentrantLock和synchronized对比
我们下面说的ReentrantLock其实就是说AQS,因为它的同步实现主要在AQS里面。
实现方面
ReentrantLock是jdk级别实现的,其源码在jdk源码中可以查看,没有脱离java。
synchronized是jvm级别实现的,synchronized只是java端的一个关键字,具体逻辑实现都在jvm中。
性能方面
优化前的synchronized性能很差,主要表现在两个方面:
因为大多数情况下对于资源的争夺并没有那么激烈,甚至于某个时刻可能只有一个线程在工作,在这种没有竞争或者竞争压力很小的情况下,如果每个线程都要进行用户态到内核态的切换其实是很耗时的。
jdk1.6对synchronized底层实现做了优化,优化后,在单线程以及并发不是很高的情况下通过无锁偏向和自旋锁的方式避免用户态到内核态的切换,因此性能提高了,优化后的synchronized和ReentrantLock性能差不多了。
ReentrantLock是在jdk实现的,它申请互斥量就是对锁标识state的争夺,它是通过cas方式实现。在java端实现。
对于争夺不到资源的线程依然要阻塞挂起,但凡阻塞挂起都要依赖于操作系统底层,这一步的用户态到内核态的切换是避免不了的。
因此在单线程进入代码块的时候,效率是很高的,因此我们说ReentrantLock性能高于原始的synchronized
申请互斥量
synchronized的锁其实就是争夺Monitor锁的拥有权,这个争夺过程是通过操作系统底层的互斥原语Mutex实现的,这个过程会有用户态到内核态的切换。
线程阻塞挂起
没能抢到到Monitor锁拥有权的线程要阻塞挂起,阻塞挂起这个动作也是依靠操作系统实现的,这个过程也需要用户态到内核态的切换。
特性方面
两个都是常用的典型的独占锁。
ReentrantLock可重入,可中断,支持公平和非公平锁,可尝试获取锁,可以支持分组将线程由不可唤醒变为可唤醒。
synchronized可重入,不可中断,非公平锁,不可尝试获取锁,只支持一个或者全部线程由不可唤醒到可唤醒。
使用方面
synchronized不需要手动释放锁,ReentrantLock需要手动释放锁,需要考虑异常对释放锁的影响避免异常导致线程一直持有锁。
以下是两个锁的使用方式
class X {
private final ReentrantLock lock = new ReentrantLock();
Condition condition1=lock.newCondition();
Condition condition2=lock.newCondition();
public void m() {
lock.lock();
try {
if(1==2){
condition1.await();
}
if(1==3){
condition2.await();
}
} catch (InterruptedException e) {
} finally {
condition1.signal();
condition2.signal();
lock.unlock();
}
}
}
class X {
private final testtest sync=new testtest();;
public void m() throws InterruptedException {
synchronized(sync){
if(1==2){
sync.wait();
}
sync.notify();
sync.notifyAll();
}
}
}
对比代码及特性说明:
两个锁都是依赖一个对象:lock和sync
condition和wait方法具有同样的效果,进入condition和wait的线程将陷入等待(不可唤醒状态),只有被分别调用signal和notify方法线程才会重新变为可唤醒状态,请注意是可唤醒,而不是被唤醒。
可唤醒是说具备了竞争资源的资格,资源空闲后,synchronized中会在可唤醒状态的线程中随机挑选一个线程去拿锁,而ReentrantLock中不可唤醒的线程变为可唤醒状态,其实就是将条件队列中的线程搬到等待队列中排队,只有队头的才会去尝试拿锁。
ReentrantLock分批将线程由不可唤醒变为可唤醒也在这段代码中体现了,代码中按照不同的条件将线程放入不同的condition,每个condition就是一个组,释放的时候也可以按照不同的条件进行释放。而synchronized中进入wait的线程不能分组,释放也只能随机释放一个或者全部释放。
很赞哦!(7444)
相关文章
- 四、长串数字域名
- 详解:DDR5内存规范和关键特性
- 在 CentOS/RHEL 7/6 上安装最新 PHP 7 软件包的 3 种方法
- 泪奔:作死尝试两天Angular+Sequelize的开发
- 付款完成后,您只需耐心等待,如果您注册成功,系统会提示您。这里需要注意的是,域名是一个即时产品,只有在最终付款成功时才能预订,注册成功后不能更改。
- 每天能省一小时!我最喜欢的9个效能工具
- 干货:如何系统学习分布式系统?
- 这个GitHub 1.4k星的Git魔法书火了丨有中文版
- 为了避免将来给我们的个人站长带来的麻烦,在选择域名后缀时,我们的站长最好省略不稳定的后缀域名,比如n,因为我们不知道策略什么时候会改变,更不用说我们将来是否还能控制这个域名了。因此,如果站长不是企业,或者有选择的话,如果不能选择域名的cn类,最好不要选择它。
- TypeScript 实战算法系列(十二):实现 Map 与 HashMap
站长推荐
3、不明先知,根据相关征兆预测可能发生的事件,以便提前做好准备,赶紧注册相关域名。;不差钱域名;buchaqian抢先注册,就是这种敏感类型。预言是最敏感的状态。其次,你应该有眼力。所谓眼力,就是善于从社会上时不时出现的各种热点事件中获取与事件相关的域名资源。眼力的前提是对域名领域的熟悉和丰富的知识。
这 6 点知识让我对 JavaScript 的对象有了更进一步的了解
使用 Linux stat 命令创建灵活的文件列表
谷歌软件工程师是怎样写设计文档的?
5. 四种状态过后,域名管理机构释放域名给公众注册。
51CTO成为华为综合性社区战略合作伙伴 双方合力打造HarmonyOS开发者生态
10个实用技巧让你的 Vue 代码更优雅
预测寿命的计算器来了:最高可延长9年寿命