在Java的异常处理机制中,finally块一直是备受开发者关注的核心话题。它被设计为无论是否发生异常、是否提前return、甚至在catch块中抛出新异常时,都能保证某些逻辑得以执行——简而言之,它就像一道强制性的安全网,确保关键操作不被遗漏。尤其是面对Lock锁或I/O资源这类需要手动管理的场景,finally块的存在几乎是不可或缺的。
有些开发者甚至直言:“synchronized无法做到这一点,但finally可以。”这句话虽然直接,却精准点出了finally在资源释放中的独特作用。
为什么必须将unlock()放在finally块中
Lock对象不像synchronized那样具备自动释放锁的机制,它需要开发者手动调用unlock()。如果天真地将unlock()写在try块末尾——例如先执行业务代码,再调用unlock——一旦业务代码中间抛出未捕获的异常,程序会直接跳转到catch块或向外抛出,unlock()就会被无情跳过。结果就是锁被某个线程长期持有,其他尝试获取该锁的线程将陷入阻塞,导致死锁或线程饥饿。
- lock.lock()必须在try外部调用:加锁操作本身可能失败(例如被中断),但释放逻辑不能因为加锁失败而被绕过。如果加锁发生在try内部,一旦加锁成功但业务代码抛异常,unlock仍会被finally捕捉到;但若加锁因异常未能成功,finally中的unlock反而会抛出IllegalMonitorStateException。因此,lock必须在try之外调用。
- 不能把unlock放进catch块:catch块只负责处理特定类型的异常,如果catch块自身也return或抛出新异常,unlock同样会被遗漏。
- 尽量避免在多个return路径中分散unlock():这种做法极易遗漏,且代码维护起来如同踩地雷,难以保证可靠性。
标准写法:Lock + try-finally
最稳妥、最通用的写法其实非常直接:
Lock lock = new ReentrantLock();
lock.lock(); // 加锁不在try内
try {
// 临界区:可能抛异常、return、continue等
doSomething();
} finally {
lock.unlock(); // 这里一定会执行
}
- 无需调用isHeldByCurrentThread()来判断当前线程是否持有锁:ReentrantLock允许非持有线程调用unlock(虽然不推荐),而且这种判断并非原子性操作,反而可能引入竞态条件风险。
- 不要在finally里throw或return:这可能会覆盖原始异常或改变方法的返回值,导致调试时难以定位问题。
- 若lock对象为null,需要在lock.lock()前做空检查:否则NPE发生在加锁阶段,就不是释放阶段的问题了。
资源类(如流、连接)的finally处理要点
面对FileInputStream、Connection等需要close()的资源,finally中的释放操作需要更加谨慎。
- 变量必须在try外部声明并初始化为null,否则finally无法访问到该变量。
- 每个资源单独判空,并单独用一个try-catch包裹关闭操作:防止一个close()抛出异常,导致后续资源无法释放。
- close()的异常应捕获并忽略(或记录日志),避免它掩盖try块中的主异常。
一个典型的示例:
InputStream in = null;
try {
in = new FileInputStream("data.txt");
// 读取操作
} catch (IOException e) {
// 处理业务异常
} finally {
if (in != null) {
try { in.close(); } catch (IOException ignored) {}
}
}
更优替代:优先用try-with-resources
对于实现了AutoCloseable接口的资源(如InputStream、Scanner、ResultSet等),try-with-resources是首选方案。它比手写finally更安全、更简洁:
- JVM会按照“后声明先关闭”的顺序自动调用close()。
- 即使try块抛出了异常,close()抛出的异常会被压制(suppressed),主异常仍然完整可见。
- 资源的作用域清晰明确,不存在变量泄漏或访问不到的问题。
- 需要注意的是,Lock本身并未实现AutoCloseable接口。如果需要类似的效果,可以自行封装一个实现了AutoCloseable的工具类。
示例:
try (FileInputStream fis = new FileInputStream("data.txt")) {
int data = fis.read();
// 使用资源
} catch (IOException e) {
// 异常处理
} // fis在此自动关闭
说到底,无论是Lock锁还是I/O资源,使用finally来确保清理操作的执行,就是Java异常处理体系中一个不可或缺的“保命牌”。掌握好它,才能让你的代码在面对不可控异常时依然保持稳健和可维护性。
