Java线程的死锁和活锁

一、概览

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

二、死锁

2.一、什么是死锁

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

经典的哲学家进餐问题很是好的展现了多线程下的同步问题而且常常被用来看成死锁的例子。并发

2.2 死锁举例

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

在这个例子中,咱们建立两个线程,T1和T2。线程T1调用operation1,线程T2调用operation2。测试

为了完成操做,线程T1须要先获取到lock1再获取到lock2,而后此时线程T2须要先获取到lock2再获取到lock1。所以两个线程都在以相反的顺序获取锁。ui

如今,咱们写一下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中是个很常见的并发问题,由于咱们应该设计一个程序来避免潜在的死锁条件。code

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

三、活锁

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,因此生产环境的队列消费最好仍是设置个失败次数加上死信队列,否则出问题可真受不了。
相关文章
相关标签/搜索