`

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多线程编程线程的协同、停止、暂停、继续等操作实现

    本篇文章将深入探讨如何实现线程的协同、停止、暂停以及继续等操作,这些都是多线程编程中的核心概念。 1. **线程的协同(协作式线程调度)** 在Java中,线程的协同主要通过`wait()`和`notify()`或`notifyAll()`...

    java多线程文件传输

    Java多线程文件传输是Java编程中一个重要的实践领域,特别是在大数据处理、网络通信和分布式系统中。在Java中,多线程可以提高程序的执行效率,尤其在处理并发任务时,如大文件的上传、下载和传输。下面将详细探讨...

    Java 多线程学习总结归纳(附代码)

    Java多线程是Java编程中的核心概念,它允许程序同时执行多个任务,从而提升系统效率。在Java中,实现多线程主要有两种方式:继承Thread类和实现Runnable接口。下面是对Java多线程学习的详细解析。 1. **多线程概述*...

    Java多线程下载器

    Java多线程下载器是一种利用Java编程语言实现的高效文件下载工具,它通过将大文件分割成多个部分并同时下载,显著提高了下载速度。在Java中实现多线程下载器涉及许多关键概念和技术,包括线程、并发控制、网络I/O...

    java多线程实现大批量数据导入源码

    本项目以"java多线程实现大批量数据导入源码"为题,旨在通过多线程策略将大量数据切分,并进行并行处理,以提高数据处理速度。 首先,我们需要理解Java中的线程机制。Java通过`Thread`类来创建和管理线程。每个线程...

    JAVA多线程编程详解-详细操作例子

    Java多线程编程是Java开发中的重要组成部分,它允许程序同时执行多个任务,提升系统效率。在Java中,实现多线程主要有两种方式:继承Thread类和实现Runnable接口。 1. 继承Thread类: 当自定义一个类继承自Thread...

    java多线程简单下载器

    【Java多线程简单下载器】是一个初学者的编程作业,虽然代码可能较为混乱,但其核心功能已经实现,即通过多线程技术进行文件的下载。在Java中,多线程是并发处理的重要手段,它允许多个任务在同一时间执行,从而提高...

    JAVA技巧(Java多线程运行时,减少内存占用量).pdf

    根据提供的文件内容,该文件主要讨论了在Java多线程环境下如何减少内存占用量。文件内容并不完整,且存在 OCR 扫描错误,但我会尝试从中提取与Java多线程和内存管理相关的知识点,并加以详细解释。 ### Java多线程...

    简单直观-实战体会Java多线程编程的精要(转)

    Java多线程编程是软件开发中的重要组成部分,尤其在服务器端和高并发应用中不可或缺。本文将基于给定的标题“简单直观-实战体会Java多线程编程的精要”来详细阐述Java多线程的核心概念、实现方式以及实战中的关键点...

    java多线程下载图片

    在这个场景中,"java多线程下载图片"意味着我们将探讨如何使用Java来实现一个能够异步下载多个图片的系统。 首先,我们需要理解Java中的线程是如何创建和运行的。Java提供了两种创建线程的方式:继承Thread类和实现...

    Java多线程详解

    Java多线程是Java编程中的核心概念,它允许程序同时执行多个任务,提高了应用程序的效率和响应速度。在Java中,多线程的实现主要有两种方式:继承Thread类和实现Runnable接口。 一、继承Thread类 当一个类直接继承...

    Java多线程机制及其在socket编程中的应用.pdf

    标题和描述中提到的“Java多线程机制及其在socket编程中的应用.pdf”,暗示了文章的两大核心内容:Java多线程技术和这些技术如何应用于socket编程。为了深入探讨这些内容,我们首先需要了解Java多线程的几个关键知识...

    Java线程创建与管理:深入理解与实践指南

    本文将详细介绍Java中创建线程的不同方法,并探讨如何有效地管理线程的生命周期,以帮助读者更好地理解和应用Java多线程编程技术。 #### Java线程基础 在Java中,线程是程序中最小的可调度单元,它可以在程序中独立...

    java多线程经典例子

    在Java多线程编程中,理解如何创建和管理线程是至关重要的。在这个经典例子中,我们看到两种创建线程的方式:通过继承`Thread`类和实现`Runnable`接口。 首先,我们创建了一个名为`ThreadUseExtends`的类,它直接...

    现代多线程 JAVA和c++多线程实现 测试和调试

    在现代软件开发中,多线程技术已经成为必不可少的一部分,特别是在JAVA和C++这样的高级编程语言中。多线程允许程序同时执行多个任务,提高应用程序的响应性和效率。本资源主要探讨了如何在JAVA和C++中实现多线程,...

    Java多线程测试程序,

    Java多线程是Java编程中的重要概念,它允许程序同时执行多个任务,提高了程序的效率和响应性。在本项目中,“Java多线程测试程序”利用了这一特性,结合图形用户界面(GUI)框架JFrame,实现了企鹅在界面上的动态...

    多线程编程的入门指导(英文版)

    多线程编程是一种在单个程序中同时执行多个任务的技术,它使得计算机系统能更高效地利用处理器资源,提高程序的响应速度和并发性。在本文中,我们将深入探讨多线程编程的基础知识,帮助初学者理解这一重要的编程概念...

    Java网络编程多线程断点下载带界面选择文件夹进度条

    Java网络编程多线程断点下载带界面选择文件夹进度条是一个高级的编程主题,它涉及到网络通信、多线程处理、文件I/O操作以及图形用户界面的设计。在这个项目中,我们将探讨以下几个关键知识点: 1. **网络编程基础**...

    Java并发编程实践(java concurrency in practice)pdf (java多线程总结.ppt)

    配合`JAVA多线程总结.ppt`,你可以得到一个更直观和简洁的概览,快速回顾和掌握上述关键知识点。虽然中文版翻译可能存在不足,但原版英文书籍通常能更准确地传达作者的意图和细节,值得深入阅读。

    java多线程断点续传下载

    通过以上讲解,我们可以看到,Java多线程断点续传下载涉及了并发编程、文件I/O、网络通信等多个领域的知识,实现起来需要综合运用多种技术。在实际开发中,理解这些原理并灵活应用,可以构建出高效且用户体验良好的...

Global site tag (gtag.js) - Google Analytics