`

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
分享到:
评论

相关推荐

    Java优化编程(第2版)

    第12章 java多线程技术与应用性能优化 12.1 java多线程技术 12.1.1 进程与线程 12.1.2 线程的生命周期 12.2 并行任务与性能 12.2.1 并行任务与多线程 12.2.2 并行任务与死锁 12.3 线程池技术与应用性能优化 12.3.1 ...

    毕业设计,关于新浪微博API的Java编程..zip

    多线程支持: Java内置了对多线程的支持,允许程序同时执行多个任务。这对于开发需要高并发性能的应用程序(如服务器端应用、网络应用等)非常重要。 自动内存管理(垃圾回收): Java具有自动内存管理机制,通过...

    【2018最新最详细】并发多线程教程

    【2018最新最详细】并发多线程教程,课程结构如下 1.并发编程的优缺点 2.线程的状态转换以及基本操作 3.java内存模型以及happens-before规则 4.彻底理解synchronized 5.彻底理解volatile 6.你以为你真的了解final吗...

    Java算法:车牌识别.zip

    多线程支持: Java内置了对多线程的支持,允许程序同时执行多个任务。这对于开发需要高并发性能的应用程序(如服务器端应用、网络应用等)非常重要。 自动内存管理(垃圾回收): Java具有自动内存管理机制,通过...

    JAVA 版本:经典PHash算法.zip

    多线程支持: Java内置了对多线程的支持,允许程序同时执行多个任务。这对于开发需要高并发性能的应用程序(如服务器端应用、网络应用等)非常重要。 自动内存管理(垃圾回收): Java具有自动内存管理机制,通过...

    (超赞)JAVA精华之--深入JAVA API

    1.7 Java 5.0多线程编程 1.8 Java Socket编程 1.9 Java的内存泄漏 1.10 抽象类与接口的区别 1.11 Java变量类型间的相互转换 2 JAVA与WEB 2.1 JMX规范 2.1.1 JMX概述 2.1.2 设备层(Instrumentation Level) 2.1.3 ...

    CS 自学指南(Java编程语言、数据库、数据结构与算法、计算机组成原理、操作系统、计算机网络、英语、简历、面试).zip

    多线程支持: Java内置了对多线程的支持,允许程序同时执行多个任务。这对于开发需要高并发性能的应用程序(如服务器端应用、网络应用等)非常重要。 自动内存管理(垃圾回收): Java具有自动内存管理机制,通过...

    java 进阶之路.zip

    多线程支持: Java内置了对多线程的支持,允许程序同时执行多个任务。这对于开发需要高并发性能的应用程序(如服务器端应用、网络应用等)非常重要。 自动内存管理(垃圾回收): Java具有自动内存管理机制,通过...

    经典遗传算法的Java实现以及遗传算法实现自动组卷.zip

    多线程支持: Java内置了对多线程的支持,允许程序同时执行多个任务。这对于开发需要高并发性能的应用程序(如服务器端应用、网络应用等)非常重要。 自动内存管理(垃圾回收): Java具有自动内存管理机制,通过...

    《数据结构与算法:Java语言描述》源码.zip

    多线程支持: Java内置了对多线程的支持,允许程序同时执行多个任务。这对于开发需要高并发性能的应用程序(如服务器端应用、网络应用等)非常重要。 自动内存管理(垃圾回收): Java具有自动内存管理机制,通过...

    java常见面试题汇总(附答案).pdf

    多线程:支持多线程编程,提高了程序的并发性能。 开源:Java 的开源性质促进了生态系统的发展和创新。 什么是 JVM,它的作用是什么? JVM(Java Virtual Machine)是 Java 虚拟机的缩写,它是 Java 程序的运行环境...

    Java精华学习资料

    Java精华学习资料 深入JAVA API 深入理解嵌套类和内部类 文件和流 java中的一些常用词汇 ...Java 5.0多线程编程 Java Socket编程 Java的内存泄漏 抽象类与接口的区别 Java变量类型间的相互转换 ……

    SM2 SM3 X.509 Cert 国密 数字签名 算法 国密证书 生成 签发 证书请求 keystore 纯java.zip

    多线程支持: Java内置了对多线程的支持,允许程序同时执行多个任务。这对于开发需要高并发性能的应用程序(如服务器端应用、网络应用等)非常重要。 自动内存管理(垃圾回收): Java具有自动内存管理机制,通过...

    博客系统:web前端+管理后台+java源码.zip

    多线程支持: Java内置了对多线程的支持,允许程序同时执行多个任务。这对于开发需要高并发性能的应用程序(如服务器端应用、网络应用等)非常重要。 自动内存管理(垃圾回收): Java具有自动内存管理机制,通过...

    数据库课设:图书馆管理系统(Java+MySQL).zip

    多线程支持: Java内置了对多线程的支持,允许程序同时执行多个任务。这对于开发需要高并发性能的应用程序(如服务器端应用、网络应用等)非常重要。 自动内存管理(垃圾回收): Java具有自动内存管理机制,通过...

    基于java的netty实现的可靠udp网络库(kcp算法),包含fec实现,可用于游戏,视频,加速等业务.zip

    多线程支持: Java内置了对多线程的支持,允许程序同时执行多个任务。这对于开发需要高并发性能的应用程序(如服务器端应用、网络应用等)非常重要。 自动内存管理(垃圾回收): Java具有自动内存管理机制,通过...

    Java面试整理,涵盖基础、JVM、线程并发、框架、MySQL、微服务、Redis、中间件、数据结构与算法等。陆续完善中.zip

    多线程支持: Java内置了对多线程的支持,允许程序同时执行多个任务。这对于开发需要高并发性能的应用程序(如服务器端应用、网络应用等)非常重要。 自动内存管理(垃圾回收): Java具有自动内存管理机制,通过...

    2021年安徽理工大学毕业设计项目:基于Java的超市管理系统.zip

    多线程支持: Java内置了对多线程的支持,允许程序同时执行多个任务。这对于开发需要高并发性能的应用程序(如服务器端应用、网络应用等)非常重要。 自动内存管理(垃圾回收): Java具有自动内存管理机制,通过...

Global site tag (gtag.js) - Google Analytics