Oracle 官方推荐,使用 ReentrantLock 需要注意的细节
前一段时间在刷 LeetCode 多线程相关题目的时候,看到使用 ReentrantLock 有两种不同的写法。
方式一:Oracle 官方推荐的写法
private val look = ReentrantLock()
fun printNumber() {
look.lock()
try {
// TODO
} finally {
look.unlock()
}
}
方式二:错误的写法
private val look = ReentrantLock()
fun printNumber() {
try {
look.lock()
} finally {
look.unlock()
}
}
方法一 是 Oracle 推荐的方式, 并且在 阿里巴巴JAVA开发手册 明确规定了不建议使用 方式二, 即不建议将 lock.lock()
写在 try...finally
代码块内部,一起来分析都有哪些需要注意的细节。
https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/ReentrantLock.html
https://github.com/alibaba/p3c/issues/287
通过这篇文章,将会学习到以下内容:
lock()
方法为什么不能放在try...finally
代码块内部?lock()
方法放在try...finally
代码块内部第一行安全吗?- 为什么要在 finally 代码块中执行
unlock()
方法? lock()
方法放在try
代码块外部一定安全吗?
lock() 方法为什么不能放在 try...finally 代码块内部
避免由于其他代码段抛出异常,造成加锁失败,导致在 finally
代码块中调用 unlock()
解锁方法,对未加锁的对象进行解锁,从而抛出 IllegalMonitorStateException
异常(依赖具体的实现), unlock()
源码很简单,如下所示。
java/util/concurrent/locks/ReentrantLock.java
public void unlock() {
sync.release(1);
}
unlock
方法被委派到了 Sync
类上,Sync
继承自 AbstractQueuedSynchronizer
。
java/util/concurrent/locks/AbstractQueuedSynchronizer.java
public final boolean release(int arg) {
if (tryRelease(arg)) {
// ......
return true;
}
return false;
}
// 供子类重写
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
子类 ReentrantLock
和 ReentrantReadWriteLock
都会重写 tryRelease
方法。这里主要看一下 ReentrantLock#tryRelease
方法。
protected final boolean tryRelease(int releases) {
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// ......
return free;
}
在 tryRelease
方法中,会判断当前线程是否等于拥有锁的线程,如果不相等则表示加锁失败,会抛出 IllegalMonitorStateException
异常。同时也会造成真正的异常信息被覆盖掉,代码如下所示。
fun printNumber() {
try {
val number = 1 / 0
look.lock()
// TODO
} finally {
look.unlock()
}
}
正如代码所示,希望出现的异常信息应该是 java.lang.ArithmeticException: / by zero
, 但是实际运行的时候,异常信息如下所示,真正的异常信息被覆盖掉了。
为什么要在 finally 代码块中执行 unlock() 方法
既然 unlock()
方法会抛出异常,为什么还要将它放在 finally
代码块中,这是为了保证执行过程中出现异常,依然能够保证锁会被释放掉,避免死锁。
注意:需要将 unlock()
方法放到 finally
代码块第一行。
lock() 方法写在 try...finally 代码块内部第一行安全吗
我在网上看到部分回答,说可以将 lock()
方法写在 try...finally
代码块内部第一行,即 lock()
方法前不会添加其他代码,但是这样真的安全吗?不一定,只能说出现问题的概率很低,一起来看一下源码描述。
根据 lock()
方法的描述,可能抛出 unchecked
异常(依赖具体的实现), 如果放在 try...finally
代码块内部,必然会触发 finally
代码块中 unlock()
方法。在 unlock()
方法中会检查是否持有锁,未持有锁则会抛出 IllegalMonitorStateException
异常(依赖具体的实现)。
根据 unlock()
方法的描述,通常只有锁的持有者才能释放锁,也就是说当非锁持有线程调用 unlock()
方法时会抛出 unchecked
异常,虽然两个方法都是因为加锁失败导致的,但是真正的异常信息会被 unlock()
方法抛出的异常信息覆盖掉。
lock() 方法写在 try
代码块外部一定安全吗
将 lock()
方法放在 try
代码块外部一定安全吗?不一定,取决于我们的代码是如何实现的,异常代码如下所示。
fun printNumber() {
look.lock()
// 抛出异常的代码
try {
// ......
} finally {
// 最后保证锁会被释放掉
look.unlock()
}
}
在加锁方法 lock()
和 try
代码块之间抛出了异常,那么就会出现加锁成功,但是无法解锁,会造成其他线程无法获取锁。
如何避免以上的问题的发生
在使用 ReentrantLock 获取锁的时候,需要注意以下几点:
look()
方法必须写在try
代码块之外look()
方法和try...finally
代码块之间,没有其他的代码段,避免出现无法解锁,造成其他线程无法获取到锁unlock()
要放到finally
代码块第一行
调用 lock() 方法,采用阻塞方式获取锁,如果失败了,则会进入阻塞等待状态
private val look = ReentrantLock()
fun printNumber() {
look.lock()
try {
// TODO
} finally {
look.unlock()
}
}
调用 tryLock() 方法,尝试去获取锁,如果失败了,则会立即返回 false
private val look = ReentrantLock()
fun printNumber() {
val isLocked = look.tryLock()
if (isLocked) {
try {
// TODO
} finally {
look.unlock()
}
}
}
技术交流,欢迎加我微信:ezglumes ,拉你入技术交流群。
私信领取相关资料
推荐阅读:
觉得不错,点个在看呗~