Oracle 官方推荐,使用 ReentrantLock 需要注意的细节

1,956 阅读6分钟

hi 大家好,我是 DHL。公众号:ByteCode ,专注分享最新技术原创文章,涉及 Kotlin、Jetpack、算法动画、数据结构 、系统源码、 LeetCode / 剑指 Offer / 多线程 / 国内外大厂算法题 等等。

前一段时间在刷 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 代码块内部,一起来分析都有哪些需要注意的细节。

通过这篇文章,将会学习到以下内容:

  • 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();
}

子类 ReentrantLockReentrantReadWriteLock 都会重写 tryRelease 方法。这里主要看一下 ReentrantLock#tryRelease 方法。
java/util/concurrent/locks/ReentrantLock.java

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()
        }
    }
}


如果有帮助 点个赞 就是对我最大的鼓励

代码不止,文章不停

欢迎关注公众号:ByteCode,持续分享最新的技术



最后推荐长期更新和维护的项目:

  • 个人博客,将所有文章进行分类,欢迎前去查看 hi-dhl.com

  • KtKit 小巧而实用,用 Kotlin 语言编写的工具库,欢迎前去查看 KtKit

  • 计划建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,正在逐渐增加 Jetpack 新成员,仓库持续更新,欢迎前去查看 AndroidX-Jetpack-Practice

  • LeetCode / 剑指 offer / 国内外大厂面试题 / 多线程 题解,语言 Java 和 kotlin,包含多种解法、解题思路、时间复杂度、空间复杂度分析

近期必读热门文章