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

共 3564字,需浏览 8分钟

 ·

2021-11-30 17:47

前一段时间在刷 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();
}

子类 ReentrantLockReentrantReadWriteLock 都会重写 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, 但是实际运行的时候,异常信息如下所示,真正的异常信息被覆盖掉了。

241a9812224277ca74c328159cc225f1.webp

为什么要在 finally 代码块中执行 unlock() 方法

既然 unlock() 方法会抛出异常,为什么还要将它放在 finally 代码块中,这是为了保证执行过程中出现异常,依然能够保证锁会被释放掉,避免死锁。

注意:需要将 unlock() 方法放到 finally 代码块第一行。

lock() 方法写在 try...finally 代码块内部第一行安全吗

我在网上看到部分回答,说可以将 lock() 方法写在 try...finally 代码块内部第一行,即  lock() 方法前不会添加其他代码,但是这样真的安全吗?不一定,只能说出现问题的概率很低,一起来看一下源码描述。

a63aa0c0ac36646d14911efc95bd9e23.webp

根据 lock() 方法的描述,可能抛出 unchecked 异常(依赖具体的实现), 如果放在 try...finally 代码块内部,必然会触发 finally 代码块中 unlock() 方法。在 unlock() 方法中会检查是否持有锁,未持有锁则会抛出 IllegalMonitorStateException 异常(依赖具体的实现)。

7cf5b02009d616f1819023faba1b9a39.webp

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

402423692b98df7012bd7dd78d17b14a.webp

技术交流,欢迎加我微信:ezglumes ,拉你入技术交流群。

私信领取相关资料

推荐阅读:

音视频开发工作经验分享 || 视频版

OpenGL ES 学习资源分享

开通专辑 | 细数那些年写过的技术文章专辑

Android NDK 免费视频在线学习!!!

你想要的音视频开发资料库来了

推荐几个堪称教科书级别的 Android 音视频入门项目

觉得不错,点个在看呗~

07e23d1a6e31d8576815e28ca0082f7e.webp
浏览 15
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报