这篇文章是并发编程系列第9集,上一次并发系列第八集通过ReentrantLock 独占锁,讲解了AQS 的基本实现原理,这次第10集开个支线,讲讲面试经常会被问的基于 AQS实现的 ReentrantLock 与 synchronized 的区别。
上海某核心商务楼里,正发生着一起求职者和面试官的较量。
面试官:你先自我介绍一下。
安琪拉:面试官你好,我是草丛三婊,最强中单(妲己不服),草地摩托车车手,第21套广播体操推广者,火的传人安琪拉,这是我的简历,可能有点沉,您拿好了,也就三十多页。
面试官:看你简历上写熟悉并发编程,熟悉到什么程度?
安琪拉:精通。对。。。问就是“精通”,头铁。
嘿嘿面试官:用过 synchronized 的吧?
安琪拉:用过,巴拉巴拉讲一大堆来之前背好的八股。
面试官:不错不错,ReentrantLock 用过吧?
安琪拉:用过,巴拉巴拉讲一大堆(这里不清楚的去背第8集)。
面试官:那你一般synchronized 用的多还是 ReentrantLock用的多?
安琪拉:用的都多
面试官:它们2 个有区别吗?什么时候用 synchronized, 什么时候用 ReentrantLock?
安琪拉:有区别,synchronized 属于非公平锁,不支持锁共享,功能比较单一。
ReentrantLock 功能比较丰富,比如需要用到公平锁、尝试加锁,带超时时间的获取锁,获取锁支持响应中断我就会用ReentrantLock。
面试官:【心想,小伙子基本功可以的】,那你能分别讲讲刚才你说的这几种的详细用法吗? 最好说说具体用的方法。
安琪拉:【问的还有点深。。。】你想先听哪个?
面试官:【有点东西】 那你讲讲ReentrantLock 支持的可以尝试加锁是意思?
安琪拉: 如果把加锁比作俘获女孩子的芳心。
synchronized 就是个痴心专一男,一旦发起加锁流程,就一心等待,即使这时女孩有男朋友(其他线程抢占),他还是会一直等待。
ReentrantLock 不一样,他有个技能,叫 tryLock,尝试获取女孩子的心,发现一旦有其他竞争对象,他立马折返撤退。
而且这个tryLock 还有个特殊技能,获取锁支持带超时时间,如果女孩在指定时间内没给机会,就会返回。
说的好听,ReentrantLock 很灵活应变,不好听,ReentrantLock 有点渣。
面试官:【陷入沉思中,原来synchronized 竟是我自己。。。】
安琪拉: 面试官,面试官,你怎么啦?
面试官:【回过神来】哦哦,你继续说,那你说下尝试加锁的实现机制。
安琪拉:很简单啦,上代码。返回尝试加锁是否成功。
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
这里很有意思,刚开始看源码的时候,我刚开始不理解为什么 nonfairTryAcquire(int acquires) 这个方法放在公共的 Sync 类里面,而不是放在非公平锁里面,如下图
函数名毕竟是 nonfair 打头的,后来看了 tryLock 的代码就懂了,无论是公平锁还是非公平锁,tryLock 都是调用的 Sync 的 nonfairTryAcquire 这个方法。
所以把 nonfairTryAcquire 翻译成非公平的尝试获取锁不是很恰当,因为它不是NonfairSync 的私有方法。
面试官:那你说下ReentrantLock 公平锁的实现机制。
安琪拉:ok。ReentrantLock 公平锁, 请看代码
//初始化ReentrantLock 的时候传入指定锁类型的boolean 值
ReentrantLock lock = new ReentrantLock(true);
//构造函数,内部可以区分公平非公平
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
面试官:能具体讲讲公平锁和非公平锁在实现机制上的区别吗?
安琪拉:可以,区别其实很简单。
公平锁,加锁前会先看下前面有没有其他线程在排队,如果前面有其他线程排队,就乖乖排在队尾。
但是非公平锁的流程是他不管前面有没有线程在排队,都先尝试获取一下锁,获取成功就直接占用。
下面是公平锁和非公平锁的二段获取锁的代码,区别很简单,公平锁多了个 hasQueuedPredecessors() 调用,先看一下队列里面是不是有其他排队者。
//公平锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// !hasQueuedPredecessors() 代表队列没有其他等待获取锁的排队线程
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;
}
//非公平锁
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//没有这个方法,直接 CAS 获取锁
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;
}
面试官:那你再说说ReentrantLock 带超时时间的获取锁的原理?
安琪拉:可以,先把超时时间转换成nano 时间单位,然后调用 doAcquireNanos 方法。
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
这个方法的主要流程和第8 集说的获取锁流程类似,只是加了一个超时时间的判断,函数入口会先用当前时间加超时间隔时间得到一个到期时间,然后在获取锁的for 循环体中判断是否超时。
大家注意到调用shouldParkAfterFailedAcquire 方法判断是否需要park的时候,如果返回true 会再判断nanosTimeout > spinForTimeoutThreshold
。
spinForTimeoutThreshold 这个参数是什么意思?
这个值是自旋的最小阈值,这里被Doug Lea 设置成了1000,表示1000纳秒,也就是说如果剩余的时间不足1000纳秒,则不需要park。
为什么不需要park,因为离到期时间太短,阻塞之后再唤醒的时间可能都不够(这里是<1000纳秒),代价还很高。
所以最好的方式是下一次for 循环要么获取到锁,要么滚蛋,不浪费一次pack 再 unpack的代价。
面试官:那你再讲讲 ReentrantLock 支持响应中断什么意思?
安琪拉:ok。ReentrantLock 的lockInterruptibly 支持中断响应,什么意思?
我们来比较一下支持中断和不支持中断的获取锁代码区别。
我们再来看下响应中断的实现方式:
只要发生了中断,直接抛 InterruptedException 异常,获取锁流程不再继续。
- 所以说ReentrantLock 如果使用 lockInterruptibly,如果发生了中断是会立即停止获取锁的流程,是响应中断的,
- 但是如果使用 lock 是不响应中断,实际上是把中断吃掉了,延迟中断(中断标识返回后主动中断)。
总结:synchronized 获取锁的过程中是不能被中断,ReentrantLock 支持。
面试官:除了你说的这几点:
公平锁
尝试加锁,获取锁带超时时间
获取锁响应中断
还有ReentrantLock 和 synchronized 还有别的区别吗?
安琪拉:【还有????你这个面试官坏的很,我知道你想问啥了。。。】
有有有,
底层实现 上,synchronized 是 JVM层面的锁,是Java关键字,通过monitor对象来完成(monitorenter与monitorexit),ReentrantLock 是从jdk1.5以来(java.util.concurrent.locks.Lock)提供的API层面的锁。
锁的对象:synchronzied 锁的是对象,锁是保存在对象头里面的,根据对象头数据来标识是否有线程获得锁/争抢锁;ReentrantLock 是根据volatile 变量 state 标识锁的获得/争抢。
实现机制上:synchronized 的实现涉及到锁的升级,具体为无锁、偏向锁、自旋锁、向内核态申请重量级锁,ReentrantLock实现则是通过利用CAS(CompareAndSwap)自旋机制保证线程操作的原子性和volatile 保证数据可见性以实现锁的功能。
释放锁方式上:synchronized 不需要用户去手动释放锁,synchronized 代码执行完后系统会自动释放锁;ReentrantLock 需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。一般通过lock() 和unlock() 方法配合 try/finally 语句块来完成。
带条件的锁:synchronized不能绑定条件;ReentrantLock 可以绑定Condition 结合await()/singal() 方法实现线程的精准唤醒,而不是像synchronized通过Object类的wait()/notify()/notifyAll() 方法要么随机唤醒一个线程要么唤醒全部线程。
面试官:那你把这几个底层实现到带条件的锁都给我一一讲清楚原理,【哈哈,你快讲,我面试要用】
安琪拉:今天讲太多,不想讲了。
面试官:那我只能表示非常遗憾了,这样吧,您先回去,后续进展会通知您的,这边出门左转。
安琪拉:【又是悲伤的一次面试经历】
ReentrantLock 与 synchronized的区别:
synchronized 属于非公平锁,ReentrantLock 支持公平锁、非公平锁;
synchronized 不支持尝试加锁,ReentrantLock 支持尝试加锁,支持带超时时间的尝试加锁;
synchronized 不支持响应中断的获取锁,ReentrantLock 提供了响应中断的加锁方法;
synchronized 不支持带条件,ReentrantLock支持;
其他底层实现上、实现机制上、锁的对象上、释放锁的方式上也有区别。