1、概览

当多线程帮助我们提高应用性能的同时,它同时也带来一些问题,本文我们将借助几个小例子看下两个问题,死锁和活锁。

2、死锁

2.1、什么是死锁

死锁发生在当两个或多个线程一直在等待另一个线程持有的锁或资源的时候。这会导致一个程序可能会被拖垮或者直接挂掉,因为线程们都不能继续工作了。

经典的哲学家进餐问题非常好的展示了多线程下的同步问题并且经常被用来当作死锁的例子。

2.2 死锁举例

首先,我们看一个简单的Java例子来理解死锁。

在这个例子中,我们创建两个线程,T1和T2。线程T1调用operation1,线程T2调用operation2。

为了完成操作,线程T1需要先获取到lock1再获取到lock2,然后此时线程T2需要先获取到lock2再获取到lock1。因此两个线程都在以相反的顺序获取锁。

现在,我们写一下DeadlockExample:

public class DeadlockExample {

    private Lock lock1 = new ReentrantLock(true);
private Lock lock2 = new ReentrantLock(true); public static void main(String[] args) {
DeadlockExample deadlock = new DeadlockExample();
new Thread(deadlock::operation1, "T1").start();
new Thread(deadlock::operation2, "T2").start();
} public void operation1() {
lock1.lock();
print("lock1 acquired, waiting to acquire lock2.");
sleep(50); lock2.lock();
print("lock2 acquired"); print("executing first operation."); lock2.unlock();
lock1.unlock();
} public void operation2() {
lock2.lock();
print("lock2 acquired, waiting to acquire lock1.");
sleep(50); lock1.lock();
print("lock1 acquired"); print("executing second operation."); lock1.unlock();
lock2.unlock();
} // helper methods
}

我们运行一下这个例子看下输出:

Thread T1: lock1 acquired, waiting to acquire lock2.
Thread T2: lock2 acquired, waiting to acquire lock1.

一运行这个例子我们就能看到程序导致了一个死锁且永远也退出不了。输出日志展示了线程T1在等待lock2,但lock2被线程T2所持有。相似的,线程T2在等待lock1,他被T1所持有。

2.3 避免死锁

死锁在Java中是个很常见的并发问题,因为我们应该设计一个程序来避免潜在的死锁条件。

  • 首先我们应该避免一个线程获取多个锁。
  • 其次如果一个线程真的需要多个锁,我们应该确保所有线程都以相同的顺序获取锁,来避免获取锁时的循环依赖问题
  • 我们也可以使用带有超时功能的锁,像Lock接口中的tryLock方法,来确保一个线程如果获取不到锁不会一直阻塞。

3、活锁

3.1 什么是活锁

活锁是另一个并发问题,它和死锁很相似。在活锁中,两个或多个线程彼此间一直在转移状态,而不像我们上个例子中互相等待。结果就是所有线程都不能执行它们各自的任务。

一个比较好的活锁例子就是消息队列。当发生异常的时候,消息消费者回滚事务并把消息放到队列头中,然后相同的消息又从队列头中被读到,又会造成异常并再次放入到队列头中。如此循坏往复,消费者永远读不到队列中其他的消息。

3.2 活锁举例

现在我们展示一下活锁的情况,我们同样拿上面死锁的例子来解释。线程T1调用operation1,线程T2调用operation2,但是我们稍微改变的操作的逻辑。

两个线程都需要拿到两把锁来完成工作,每个线程拿到第一个锁后都会发现拿不到第二把锁,因此为了让另一个线程先完成任务,每个线程都会释放第一把锁并会尝试再次获取到两把锁。

我们来看下下面的测试例子

public class LivelockExample {
 
    private Lock lock1 = new ReentrantLock(true);
    private Lock lock2 = new ReentrantLock(true);
 
    public static void main(String[] args) {
        LivelockExample livelock = new LivelockExample();
        new Thread(livelock::operation1, "T1").start();
        new Thread(livelock::operation2, "T2").start();
    }
 
    public void operation1() {
        while (true) {
            tryLock(lock1, 50);
            print("lock1 acquired, trying to acquire lock2.");
            sleep(50);
 
            if (tryLock(lock2)) {
                print("lock2 acquired.");
            } else {
                print("cannot acquire lock2, releasing lock1.");
                lock1.unlock();
                continue;
            }
 
            print("executing first operation.");
            break;
        }
        lock2.unlock();
        lock1.unlock();
    }
 
    public void operation2() {
        while (true) {
            tryLock(lock2, 50);
            print("lock2 acquired, trying to acquire lock1.");
            sleep(50);
 
            if (tryLock(lock1)) {
                print("lock1 acquired.");
            } else {
                print("cannot acquire lock1, releasing lock2.");
                lock2.unlock();
                continue;
            }
 
            print("executing second operation.");
            break;
        }
        lock1.unlock();
        lock2.unlock();
    }
 
    // helper methods
}

我们看下运行结果:

Thread T1: lock1 acquired, trying to acquire lock2.
Thread T2: lock2 acquired, trying to acquire lock1.
Thread T1: cannot acquire lock2, releasing lock1.
Thread T2: cannot acquire lock1, releasing lock2.
Thread T2: lock2 acquired, trying to acquire lock1.
Thread T1: lock1 acquired, trying to acquire lock2.
Thread T1: cannot acquire lock2, releasing lock1.
Thread T1: lock1 acquired, trying to acquire lock2.
Thread T2: cannot acquire lock1, releasing lock2.

能看到输出结果里,两个线程都在重复的获取锁和释放锁,导致两个线程都不能完成操作。

3.3 避免活锁

避免活锁我们得观察一下活锁发生的条件并根据情况提出方案,比如:

  • 如果我们有两个线程在重复的获取锁和释放锁导致了活锁,我们可以修改下代码让两个线程以一个随机的时间间隔来获取锁,这样线程就有机会获取到它们需要的锁了。
  • 另一个方式来解决我们前面提到的消息队列的问题就是把失败的消息放到单独的的队列中去进一步处理而不是再次放入原队列中。(这个在实际开发中还真遇到过,开发的时候漏掉了一个情况,导致循环消费多条错误消息,队列消息大量积压,要不是队列报警,险些造成线上bug,所以生产环境的队列消费最好还是设置个失败次数加上死信队列,不然出问题可真受不了。

Java线程的死锁和活锁的更多相关文章

  1. java 线程的死锁问题

    以下的情况可能出现死锁 1.一个对象的同步方法去调用另一个对象的同步方法,同时另一个对象的同步方法也在调用这个对象的同步方法,导致一定几率的死锁,不一定每次都会出现死锁,模拟的代码如下 package ...

  2. Java线程堆栈分析

    不知觉间工作已有一年了,闲下来的时候总会思考下,作为一名Java程序员,不能一直停留在开发业务使用框架上面.老话说得好,机会是留给有准备的人的,因此,开始计划看一些Java底层一点的东西,尝试开始在学 ...

  3. 不依赖jstack的java 线程dump和死锁检查工具

    java线程dump可以使用jdk的命令"jstack  pid"完成,死锁检查可以用jconsole查看到.这两个工具是java调试的常用方法. 我遇到的问题是:在sles11s ...

  4. Java如何使用线程解决死锁?

    在Java编程中,如何使用线程解决死锁? 以下示例演示如何使用线程的概念解决死锁问题. // from W w w .Y I I b AI.c o M package com.yiibai; impo ...

  5. Java线程死锁查看分析方法

    如何查看是否有Java线程死锁?下面介绍两种方法. 一.Jconsole        Jconsole是JDK自带的图形化界面工具,使用JDK给我们的的工具JConsole,可以通过打开cmd然后输 ...

  6. java多线程中的死锁、活锁、饥饿、无锁都是什么鬼?

    死锁.活锁.饥饿是关于多线程是否活跃出现的运行阻塞障碍问题,如果线程出现了这三种情况,即线程不再活跃,不能再正常地执行下去了. 死锁 死锁是多线程中最差的一种情况,多个线程相互占用对方的资源的锁,而又 ...

  7. Java线程和多线程(九)——死锁

    Java中的死锁指的就是一种多于两个线程永远阻塞的特殊状况.Java中的死锁状态至少需要多于两个线程以及资源的时候才会产生.这里,我写了一个产生死锁的程序,并且讲下如何分析死锁. 首先来看一下产生死锁 ...

  8. Java多线程——线程的死锁

    Java多线程——线程的死锁 摘要:本文主要介绍了Java多线程中遇到的死锁问题. 部分内容来自以下博客: https://www.cnblogs.com/wy697495/p/9757982.htm ...

  9. java 线程死锁的检测

    java 线程死锁的检测   例子程序: import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executo ...

随机推荐

  1. 备战noip week8

    POJ1144 网络 description: 给出一张\(N\)个点的无向图,求其中割点的个数 data range: \(N\le 100\) solution: 一道模板题(但是读入实在是把我恶 ...

  2. 浅析软件测试人员如何对JVM进行内存溢出检测

    一.什么是JVM,检测JVM的意义 JVM是java virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各 ...

  3. JVM详解(二)-- 第2章 类加载器子系统

    一.JVM内存结构 1.1 内存结构---概略图 1.2 内存结构--详细图 二.类加载器子系统的作用 类加载器子系统负责从文件系统或网络中加载.Class文件,文件需要有特定的标识(cafe bab ...

  4. NB-IOT覆盖能力有多强 是怎么实现的

    NB-IoT技术中出现以来就以其强大的覆盖能力和通信距离长而受到广大使用者的欢迎,那么NB-IoT覆盖能力究竟是有多大,其覆盖能力应该怎么来衡量? 强大的覆盖能力是NB-IoT技术的最大特点之一,不仅 ...

  5. Java学习的第八天

    1.求平均值 冒泡法 选择排序 插入排序 快速排序 二分法查找 使用工具排序 工具二分法查找 生成随机数 2.在二分法时,没有强调要先排序才可以二分法.递归没太看明白. 3.明天学习综合实例和第四章开 ...

  6. python爬虫爬取策略

    爬取策略 关注公众号"轻松学编程"了解更多. 在爬虫系统中,待抓取URL队列是很重要的一部分.待抓取URL队列中的URL以什么样的顺序排列也是一个很重要的问题,因为这涉及到先抓取那 ...

  7. Lock接口之Condition接口

    之前在写显示锁的是后,在显示锁的接口中,提到了new Condition这个方法,这个方法会返回一个Condition对象 简单介绍一下 Condition接口: 任意一个Java对象,都拥有一组监视 ...

  8. 前端之旅一:vscode调试web配置

    vscode调试web配置 第一步:安装vscode,并启动(vscode的安装包自行到其官网上下载) 第二步:在vscode上安装chrome插件 第三步:配置,启动调试 配置信息 { // Use ...

  9. python开发--基础知识-(持续更新)

    python基础 --基础: 1, 第一句python - 用cmd 调用--python (路径)+(文件名)) 扩展名是任意的 - 导入模块是,如果不是.py文件,可能导入不成功 - python ...

  10. Mybatis 批量更新遇到的小问题

    小问题 记一个开发过程中因为小细节的遗漏而引发的 "莫名其妙",公司中有个2B(to B)供应链项目,持久层用的是 JPA,这里我就不吐槽 JPA 了,这种 SQL 嵌入在代码里的 ...