90% 的 Java 程序员都会犯的错
hi
这是 dhl
的第 86 篇文章个人微信: hi-dhl
Hi 大家好,我是 DHL,大厂全栈程序员,就职于 美团、快手、小米。分享技术干货和编程知识点,包含性能优化、系统源码、算法、数据结构、大厂面经
视频版
文字版
方式一: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
代码块内部,一起来分析都有哪些需要注意的细节。
通过这篇文章,将会学习到以下内容:
lock()
方法为什么不能放在try...finally
代码块内部?lock()
方法放在try...finally
代码块内部第一行安全吗?为什么要在 finally 代码块中执行
unlock()
方法?lock()
方法放在try
代码块外部一定安全吗?
Lock () 方法为什么不能放在 try... Finally 代码块内部
避免由于其他代码段抛出异常,造成加锁失败,导致在 finally
代码块中调用 unlock()
解锁方法,对未加锁的对象进行解锁,从而抛出 IllegalMonitorStateException
异常(依赖具体的实现), unlock()
源码很简单,如下所示。java/util/concurrent/locks/ReentrantLock. Java
sync.release(1);
}
unlock
方法被委派到了 Sync
类上,Sync
继承自 AbstractQueuedSynchronizer
。java/util/concurrent/locks/AbstractQueuedSynchronizer. Java
if (tryRelease(arg)) {
// ......
return true;
}
return false;
}
// 供子类重写
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
子类 ReentrantLock
和 ReentrantReadWriteLock
都会重写 tryRelease
方法。这里主要看一下 ReentrantLock#tryRelease
方法。java/util/concurrent/locks/ReentrantLock. Java
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// ......
return free;
}
在 tryRelease
方法中,会判断当前线程是否等于拥有锁的线程,如果不相等则表示加锁失败,会抛出 IllegalMonitorStateException
异常。同时也会造成真正的异常信息被覆盖掉,代码如下所示。
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
代码块外部一定安全吗
既然 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
代码块外部一定安全吗?不一定,取决于我们的代码是如何实现的,异常代码如下所示。
look.lock()
// 抛出异常的代码
try {
// ......
} finally {
// 最后保证锁会被释放掉
look.unlock()
}
}
在加锁方法 lock()
和 try
代码块之间抛出了异常,那么就会出现加锁成功,但是无法解锁,会造成其他线程无法获取锁。
如何避免以上的问题的发生
在使用 ReentrantLock 获取锁的时候,需要注意以下几点:
look()
方法必须写在try
代码块之外look()
方法和try...finally
代码块之间,没有其他的代码段,避免出现无法解锁,造成其他线程无法获取到锁unlock()
要放到finally
代码块第一行
全文到这里就结束了,感谢你的阅读,坚持原创不易,欢迎 在看、点赞、分享 给身边的小伙伴,我会持续分享原创干货!!!
推荐阅读:
适配 Android 14,功能和权限的变更,你的应用受影响了吗
Hi 大家好,我是 DHL,就职于 美团、快手、小米。分享技术干货和编程知识点,包含性能优化、系统源码、算法、数据结构、大厂面经。
哔哩哔哩:https://space.bilibili.com/498153238
掘金:https://juejin.im/user/2594503168898744
博客:https://hi-dhl.com
Github:https://github.com/hi-dhl
👇🏻 真诚推荐你关注我👇🏻
因微信公众号更改了推送机制
可能无法及时看到最新文章
将公众号设为 星标
或常为文章点 在看
即可及时收到最新文章
欢迎前往 博客 查看更多 Kotlin、Jetpack 、动画算法图解、系统源码分析等等文章。以及开源项目、LeetCode / 剑指 offer / 国内外大厂面试题 / 多线程 题解。
https://www.hi-dhl.com