美文网首页
Java线程的死锁和活锁

Java线程的死锁和活锁

作者: Mr靖哥哥 | 来源:发表于2020-06-23 23:40 被阅读0次

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面试:多线程中的各种锁,你了解几个?

    学习 java 多线程时,最头疼的知识点之一就是 java 中的锁了,什么互斥锁、排它锁、自旋锁、死锁、活锁等等,...

  • Java线程的死锁和活锁

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

  • Java并发编程73道面试题快速一览

    1、在java中守护线程和本地线程区别? 2、线程与进程的区别? 3、什么是多线程中的上下文切换? 4、死锁与活锁...

  • 死锁与问题排查

    本篇文章从Java线程、锁层面去考虑考虑死锁。 死锁:多个线程,彼此持有对方需要的锁资源,谁也不肯释放,谁也无法进...

  • 多线程高并发

    1. 进程和线程的区别 2. synchronized 的锁升级 3. 多线程写一个死锁 4. java有哪些锁,...

  • 如何去检测死锁

    如何检测死锁 死锁预防 让线程获取锁的顺序一致 死锁检测 jps 查看java 进程信息 jstack +进程号 ...

  • Synchronized关键字详解2(对非object持锁)

    4.死锁:同步方法容易造成死锁。Java线程死锁是一个经典的多线程问题,因为不同的线程都在等待根本不可能被释放的锁...

  • 死锁

    线程饥饿死锁 锁顺序死锁 动态锁顺序死锁通过锁顺序来避免死锁 避免死锁

  • 高并发编程-05-活跃性问题

    死锁,饥饿,活锁 1,死锁 多个线程,各自占对方的资源,都不愿意释放,从而造成死锁 工具:使用jconsole可以...

  • 并发 :线程间同步、锁、可重入锁及互斥锁

    线程间同步 线程间同步涉及线程互斥锁; 锁(Lock)容易导致死锁,可重入锁(RLock)则不会导致死锁,但每次 ...

网友评论

      本文标题:Java线程的死锁和活锁

      本文链接:https://www.haomeiwen.com/subject/dwxlfktx.html