`

Java多线程编程那些事:锁泄漏

阅读更多

什么是锁泄漏

众所周知,我们对锁的使用方式都是用同一个套路——先申请锁,再执行临界区中的代码,最后释放锁,如清单1所示。尽管如此,代码的错误可能导致一个线程在其执行完临界区代码之后未能释放引导这个临界区的锁。例如,清单1中的doSomethingWithLock方法所调用的someIoOperation方法如果在其执行期间抛出了异常(这里是IOException),那么doSomethingWithLock方法中的释放锁的语句将不会被执行,即此时doSomethingWithLock方法的执行线程在执行完临界区代码之后并没有释放引导该临界区的锁lock,这种现象(故障)就被称为锁泄漏(Lock Leak)。锁泄漏会导致其他线程无法获得其所需的锁,从而使得这些线程都无法完成其任务。

清单1 锁泄漏示例代码

/**
 * 本代码是为演示“锁泄漏”而特意依照错误的方式编写的。
 * 
 * @author viscent
 *
 */
public class LockLeakExample {
    ReentrantLock lock = new ReentrantLock();

    // ...
    public static void main(String[] args) {
        LockLeakExample example = new LockLeakExample();

        Thread t;
        for (int i = 0; i < 24; i++) {
            t = new Thread(new Runnable() {

                @Override
                public void run() {
                    try {
                        example.doSomethingWithLock();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }

                }

            });

            t.start();

        }

    }

    public void doSomethingWithLock() throws IOException {
        lock.lock();// 申请锁
        // 临界区开始
        someIoOperation();
        // 临界区结束
        lock.unlock();// 释放锁
    }

    public void someIoOperation() throws IOException {
        // ...
    }

}

 

值得注意的是,锁泄漏可能并不总是像上述例子那样经过分析显得那么明显,锁泄漏具有一定的隐蔽性——即使代码中存在锁泄漏,但是这种故障并不一定能够被我们察觉,而等到我们察觉的时候可能为时已晚(比如系统已经上线)。下一节我们会进一步介绍这点。不过,锁泄漏的规避方法非常简单:对于上述例子中我们只需要将锁的释放这个操作放在一个try-finally语句的finally块中就可以锁泄漏,如清单2所示。

 

清单2 避免锁泄漏示例代码

public class LockleakAvoidance {
    ReentrantLock lock = new ReentrantLock();

    // ...
    public static void main(String[] args) {
        LockLeakExample example = new LockLeakExample();

        Thread t;
        for (int i = 0; i < 24; i++) {
            t = new Thread(new Runnable() {

                @Override
                public void run() {
                    try {
                        example.doSomethingWithLock();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }

                }

            });

            t.start();

        }

    }

    public void doSomethingWithLock() throws IOException {
        lock.lock();// 申请锁
        try {
            // 临界区开始
            someIoOperation();
            // 临界区结束
        } finally {
            lock.unlock();// 确保“释放锁”这个操作总是能够被执行到
        }
    }

    public void someIoOperation() throws IOException {
        // ...
    }

}

锁泄漏的隐蔽性——可重入锁

Java平台中的所有锁都是可重入的(Reentrant),这使得锁泄漏具有一定的隐蔽性——即使释放锁的操作没有被正确地放在finally块中,并且临界区中的代码执行过程中也抛出了异常,锁泄漏所导致的后果(其他线程无法获得锁)也不一定就能立刻显现出来。所谓可重入,是指一个线程在持有一个锁的情况下仍然可以继续申请这个锁,并且这个线程总是可以成功申请到(获得)这个锁(确切的说,是有一定的次数限制的)。对于清单1中的doSomethingWithLock方法,在系统的并发量极小的情况下极有可能始终只有一个线程在执行该方法,那么即使doSomeIoOperation方法在其执行过程中抛出异常而导致这个线程未能释放锁lock,由于Java平台中的锁是可重入的,该线程后续再次执行doSomethingWithLock方法仍然可以继续获得锁lock,这就一定程度上掩盖了锁泄漏。这种情形下,锁泄漏所导致的后果只有等到系统并发量增大到多于1个线程执行doSomethingWithLock方法才能够显现出来。

锁泄漏免疫——内部锁

Java平台中的显式锁(Lock接口的实现类)的不当使用会造成锁泄漏,但是内部锁(synchronized)的使用不会造成锁泄漏。对于内部锁,无论其所引导的临界区中的代码是正常退出还是由于抛出异常而退出,Java平台会保证这个内部锁总是会被释放。Java平台对内部锁的这种保障实际上是由静态编译器(javac)来实现的。下面,我们通过查看清单3中的doSomethingWithLock方法对应的字节码(Byte Code)来证实这点。

 

清单3 使用内部锁避免锁泄漏

public class SynchronizedLockLeakFree {

    // ...
    public static void main(String[] args) {
        SynchronizedLockLeakFree example = new SynchronizedLockLeakFree();

        Thread t;
        for (int i = 0; i < 24; i++) {
            t = new Thread(new Runnable() {

                @Override
                public void run() {
                    try {
                        example.doSomethingWithLock();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }

                }

            });

            t.start();

        }

    }

    public void doSomethingWithLock() throws IOException {
        synchronized (this) {// 申请锁
            // 临界区开始
            someIoOperation();
            // 临界区结束
        }// 释放锁
    }

    public void someIoOperation() throws IOException {
        // ...
    }

}

 

清单3中的doSomethingWithLock方法对应的字节码如下所示:

 

public void doSomethingWithLock() throws java.io.IOException;
 Code:
    0: aload_0       
    1: dup           
    2: astore_1      
    3: monitorenter  
    4: aload_0       
    5: invokevirtual #43;                // Method someIoOperation:()V
    8: aload_1       
    9: monitorexit   
   10: goto          16
   13: aload_1       
   14: monitorexit   
   15: athrow        
   16: return        
 Exception table:
    from    to  target type
        4    10    13   any
       13    15    13   any

 

上面的字节码中,每一行代码中“:”后面的字符串代表Java虚拟机的指令,“:”前面的数字代表指令相对于其所在的方法的偏移位置(字节)。monitorenter和monitorexit这两个指令的作用分别是申请内部锁和释放内部锁,athrow指令的作用抛出异常。当临界区中的代码没有产生异常时,代码的执行路径是3->4->5->8->9,即“申请锁->调用someIoOperation方法->释放锁”。从上述异常表(Exception Table)中可以看出,位于4字节到10字节之间的指令执行时若产生异常,则代码会转到位于13字节处的指令继续执行。因此,如果临界区中的代码(即someIoOperation方法调用)执行时产生了异常,那么此时代码的执行路径会是3->4->5->13->14->15。由此可见,Java虚拟机会在抛出异常前执行monitorexit指令以释放内部锁。

用模板方法模式避免锁泄漏

使用显式锁的时候,为了避免锁泄漏我们必须确保线程在退出临界区后一定会释放锁。但是,直接使用try-catch-finally语句来确保这点存在两个问题:首先,这种方法是不太可靠的,新手甚至于“老手”容易忘记将Lock.unlock()调用放在finally块中;其次,这种方法会导致大量的样板式(Boilerplate)代码,这违反了DRY(Don’t Repeat Yourself)原则。有鉴于此,我们考虑可以使用模板方法(Template Method)模式来避免锁泄漏,如清单4所示。

清单4 使用模板方法模式避免锁泄漏

public class LockTemplate {

    final protected ReentrantLock lock;

    public LockTemplate(ReentrantLock lock) {

        this.lock = lock;

    }

    public LockTemplate() {

        this(new ReentrantLock());

    }

    public void doWithLock(Runnable task) {

        lock.lock();

        try {

            task.run();

        } finally {

            lock.unlock();
        }
    }
}

有了LockTemplate这个工具之后,我们可以使用一个Runnable实例来表示临界区中的代码,而锁的申请与释放则由LockTemplate.doWithLock来考虑。

总结

锁泄漏是代码错误导致的一个线程未能释放其持有的锁从而导致其他线程无法获得相应锁的故障。内部锁的使用不会导致锁泄漏,显式锁使用不当会导致锁泄漏。Lock.unlock()总是应该被放到finally块中。模板方法模式可以用来避免锁泄漏。

参考资料

1、 黄文海.Java多线程编程实战指南(核心篇).电子工业出版社,2017

微信公众号:VChannel

 

0
0
分享到:
评论

相关推荐

    spring-boot-2.3.0.RELEASE.jar中文-英文对照文档.zip

    # 压缩文件中包含: 中文-英文对照文档 jar包下载地址 Maven依赖 Gradle依赖 源代码下载地址 # 本文件关键字: jar中文-英文对照文档.zip,java,jar包,Maven,第三方jar包,组件,开源组件,第三方组件,Gradle,中文API文档,手册,开发手册,使用手册,参考手册 # 使用方法: 解压最外层zip,再解压其中的zip包,双击 【index.html】 文件,即可用浏览器打开、进行查看。 # 特殊说明: ·本文档为人性化翻译,精心制作,请放心使用。 ·只翻译了该翻译的内容,如:注释、说明、描述、用法讲解 等; ·不该翻译的内容保持原样,如:类名、方法名、包名、类型、关键字、代码 等。 # 温馨提示: (1)为了防止解压后路径太长导致浏览器无法打开,推荐在解压时选择“解压到当前文件夹”(放心,自带文件夹,文件不会散落一地); (2)有时,一套Java组件会有多个jar,所以在下载前,请仔细阅读本篇描述,以确保这就是你需要的文件;

    C接口函数参考手册样本.doc

    C接口函数参考手册样本.doc

    scratch少儿编程逻辑思维游戏源码-小小王国.zip

    scratch少儿编程逻辑思维游戏源码-小小王国.zip

    mysql全国区域代码表3级

    mysql全国区域代码表3级

    scratch少儿编程逻辑思维游戏源码-我的世界 地牢通关.zip

    scratch少儿编程逻辑思维游戏源码-我的世界 地牢通关.zip

    spring-boot-1.5.14.RELEASE.jar中文-英文对照文档.zip

    # 压缩文件中包含: 中文-英文对照文档 jar包下载地址 Maven依赖 Gradle依赖 源代码下载地址 # 本文件关键字: jar中文-英文对照文档.zip,java,jar包,Maven,第三方jar包,组件,开源组件,第三方组件,Gradle,中文API文档,手册,开发手册,使用手册,参考手册 # 使用方法: 解压最外层zip,再解压其中的zip包,双击 【index.html】 文件,即可用浏览器打开、进行查看。 # 特殊说明: ·本文档为人性化翻译,精心制作,请放心使用。 ·只翻译了该翻译的内容,如:注释、说明、描述、用法讲解 等; ·不该翻译的内容保持原样,如:类名、方法名、包名、类型、关键字、代码 等。 # 温馨提示: (1)为了防止解压后路径太长导致浏览器无法打开,推荐在解压时选择“解压到当前文件夹”(放心,自带文件夹,文件不会散落一地); (2)有时,一套Java组件会有多个jar,所以在下载前,请仔细阅读本篇描述,以确保这就是你需要的文件;

    1-附件2:两个医院随访的抗抑郁药使用后主诉情况.xlsx

    两个医院随访的抗抑郁药使用后主诉情况

    一个轻量级、可移植的纯 NES 仿真器库

    一个轻量级、可移植的纯 NES 仿真器库

    spring-boot-2.4.9.jar中文文档.zip

    # 压缩文件中包含: 中文文档 jar包下载地址 Maven依赖 Gradle依赖 源代码下载地址 # 本文件关键字: jar中文文档.zip,java,jar包,Maven,第三方jar包,组件,开源组件,第三方组件,Gradle,中文API文档,手册,开发手册,使用手册,参考手册 # 使用方法: 解压最外层zip,再解压其中的zip包,双击 【index.html】 文件,即可用浏览器打开、进行查看。 # 特殊说明: ·本文档为人性化翻译,精心制作,请放心使用。 ·只翻译了该翻译的内容,如:注释、说明、描述、用法讲解 等; ·不该翻译的内容保持原样,如:类名、方法名、包名、类型、关键字、代码 等。 # 温馨提示: (1)为了防止解压后路径太长导致浏览器无法打开,推荐在解压时选择“解压到当前文件夹”(放心,自带文件夹,文件不会散落一地); (2)有时,一套Java组件会有多个jar,所以在下载前,请仔细阅读本篇描述,以确保这就是你需要的文件;

    spring-boot-1.5.19.RELEASE.jar中文-英文对照文档.zip

    # 压缩文件中包含: 中文-英文对照文档 jar包下载地址 Maven依赖 Gradle依赖 源代码下载地址 # 本文件关键字: jar中文-英文对照文档.zip,java,jar包,Maven,第三方jar包,组件,开源组件,第三方组件,Gradle,中文API文档,手册,开发手册,使用手册,参考手册 # 使用方法: 解压最外层zip,再解压其中的zip包,双击 【index.html】 文件,即可用浏览器打开、进行查看。 # 特殊说明: ·本文档为人性化翻译,精心制作,请放心使用。 ·只翻译了该翻译的内容,如:注释、说明、描述、用法讲解 等; ·不该翻译的内容保持原样,如:类名、方法名、包名、类型、关键字、代码 等。 # 温馨提示: (1)为了防止解压后路径太长导致浏览器无法打开,推荐在解压时选择“解压到当前文件夹”(放心,自带文件夹,文件不会散落一地); (2)有时,一套Java组件会有多个jar,所以在下载前,请仔细阅读本篇描述,以确保这就是你需要的文件;

    scratch少儿编程逻辑思维游戏源码-像素猫3.zip

    scratch少儿编程逻辑思维游戏源码-像素猫3.zip

    自来水厂水处理系统的WinCC组态与西门子300PLC程序解析及其应用

    内容概要:本文深入探讨了自来水厂水处理系统的自动化解决方案,重点介绍了WinCC组态软件和西门子300PLC程序的具体应用。文中首先简述了自来水厂作为城市供水系统核心的作用,随后详细讲解了WinCC组态软件在实时监控和控制方面的功能,以及西门子300PLC在数据处理、控制逻辑和通信等方面的关键角色。接着,文章具体阐述了水源采集与处理、水质检测与监控、自动化控制系统三个主要环节的技术实现方式。最后,强调了西门子PLC技术在提高生产效率、实时监控与预警、优化生产流程等方面的优势。 适合人群:从事工业自动化领域的工程师和技术人员,尤其是关注水处理行业的专业人士。 使用场景及目标:适用于希望深入了解自来水厂自动化控制系统的读者,旨在帮助他们掌握WinCC组态和西门子300PLC的实际应用方法,提升水处理系统的运行效率和安全性。 其他说明:本文不仅提供了理论知识,还结合实际案例进行了解析,有助于读者更好地理解和应用相关技术。

    三菱PLC FX2N源码与STM32工控板结合的高性能工业控制解决方案

    内容概要:本文介绍了三菱PLC FX2N源码V9.x版与STM32工控板的结合,展示了其强大功能和应用场景。三菱PLC FX2N源码提供清晰的程序架构和详细的注释,支持多种指令如DECO、ENCO、SEGD,并优化了RTC时间功能。STM32工控板则支持2路高速脉冲输出、2路外部脉冲输入、2路485 Modbus RTU主从机模式等功能。两者结合为工业控制带来了更高的性能和更多样化的功能。 适合人群:从事工业自动化领域的工程师和技术人员,尤其是对三菱PLC和STM32有研究兴趣的人群。 使用场景及目标:适用于需要高效、稳定工业控制系统的设计和开发,旨在提升工业设备的精确控制能力,提高生产效率和质量。 其他说明:文中提到的具体功能和特性有助于开发者更好地理解和应用三菱PLC FX2N源码与STM32工控板的结合,推动工业自动化的发展。

    三相下垂双逆变器同步并联控制技术的研究与应用

    内容概要:本文深入探讨了三相下垂双逆变器同步并联控制技术,重点介绍了下垂控制的基本原理及其在微电网中的应用。文章详细解释了下垂控制如何通过调整频率和电压幅值来实现负载的自动分配,并讨论了在多台逆变器并联时可能出现的环流问题以及解决方案,如虚拟阻抗法。此外,还介绍了同步环节的关键技术,特别是改进型锁相环的应用,并提供了具体的实现代码示例。最后,文章分享了一些实用的调试技巧和经验,强调了参数整定的重要性。 适用人群:从事电力电子、微电网控制领域的研究人员和技术人员。 使用场景及目标:适用于希望深入了解三相下垂双逆变器同步并联控制技术的工程师和科研人员,旨在帮助他们掌握核心技术,解决实际工程中的问题。 其他说明:文中提供的代码示例和调试方法有助于读者更好地理解和应用相关技术,提高系统的稳定性和性能。

    Google Python Style

    Google Python Style

    spring-boot-1.2.5.RELEASE.jar中文-英文对照文档.zip

    # 压缩文件中包含: 中文-英文对照文档 jar包下载地址 Maven依赖 Gradle依赖 源代码下载地址 # 本文件关键字: jar中文-英文对照文档.zip,java,jar包,Maven,第三方jar包,组件,开源组件,第三方组件,Gradle,中文API文档,手册,开发手册,使用手册,参考手册 # 使用方法: 解压最外层zip,再解压其中的zip包,双击 【index.html】 文件,即可用浏览器打开、进行查看。 # 特殊说明: ·本文档为人性化翻译,精心制作,请放心使用。 ·只翻译了该翻译的内容,如:注释、说明、描述、用法讲解 等; ·不该翻译的内容保持原样,如:类名、方法名、包名、类型、关键字、代码 等。 # 温馨提示: (1)为了防止解压后路径太长导致浏览器无法打开,推荐在解压时选择“解压到当前文件夹”(放心,自带文件夹,文件不会散落一地); (2)有时,一套Java组件会有多个jar,所以在下载前,请仔细阅读本篇描述,以确保这就是你需要的文件;

    scratch少儿编程逻辑思维游戏源码-像素竞技场.zip

    scratch少儿编程逻辑思维游戏源码-像素竞技场.zip

    scratch少儿编程逻辑思维游戏源码-星之卡比(2).zip

    scratch少儿编程逻辑思维游戏源码-星之卡比(2).zip

    spring-boot-2.3.5.RELEASE.jar中文-英文对照文档.zip

    # 压缩文件中包含: 中文-英文对照文档 jar包下载地址 Maven依赖 Gradle依赖 源代码下载地址 # 本文件关键字: jar中文-英文对照文档.zip,java,jar包,Maven,第三方jar包,组件,开源组件,第三方组件,Gradle,中文API文档,手册,开发手册,使用手册,参考手册 # 使用方法: 解压最外层zip,再解压其中的zip包,双击 【index.html】 文件,即可用浏览器打开、进行查看。 # 特殊说明: ·本文档为人性化翻译,精心制作,请放心使用。 ·只翻译了该翻译的内容,如:注释、说明、描述、用法讲解 等; ·不该翻译的内容保持原样,如:类名、方法名、包名、类型、关键字、代码 等。 # 温馨提示: (1)为了防止解压后路径太长导致浏览器无法打开,推荐在解压时选择“解压到当前文件夹”(放心,自带文件夹,文件不会散落一地); (2)有时,一套Java组件会有多个jar,所以在下载前,请仔细阅读本篇描述,以确保这就是你需要的文件;

    spring-boot-2.6.5.jar中文文档.zip

    # 压缩文件中包含: 中文文档 jar包下载地址 Maven依赖 Gradle依赖 源代码下载地址 # 本文件关键字: jar中文文档.zip,java,jar包,Maven,第三方jar包,组件,开源组件,第三方组件,Gradle,中文API文档,手册,开发手册,使用手册,参考手册 # 使用方法: 解压最外层zip,再解压其中的zip包,双击 【index.html】 文件,即可用浏览器打开、进行查看。 # 特殊说明: ·本文档为人性化翻译,精心制作,请放心使用。 ·只翻译了该翻译的内容,如:注释、说明、描述、用法讲解 等; ·不该翻译的内容保持原样,如:类名、方法名、包名、类型、关键字、代码 等。 # 温馨提示: (1)为了防止解压后路径太长导致浏览器无法打开,推荐在解压时选择“解压到当前文件夹”(放心,自带文件夹,文件不会散落一地); (2)有时,一套Java组件会有多个jar,所以在下载前,请仔细阅读本篇描述,以确保这就是你需要的文件;

Global site tag (gtag.js) - Google Analytics