什么是死锁, 活锁, 饥饿, 无锁

core-java
标签: #<Tag:0x00007f1d2830af38>

#1

死锁 Deadlock

触发条件

  • 在一个线程里需要同时获得多个对象锁时.
  • 上边提到的多个对象锁会被多个线程访问.

以下这个情况必然会发生死锁.

假设有两个线程 T1 和 T2 需要同时获得两个共享对象 O1 和 O2 (也就是说需要同时得到 O1 和 O2 的锁).

  • 当两个线程同时启动后, T1 先访问 O1 (得到了 O1 的锁)
  • 同时 T2 访问 O2 (得到了 O2 的锁).
  • 然后 T1 去请求 O2 的对象锁. 因为这时 O2 的锁被 T2 占有, 所以 T1 进入阻塞状态, 等待 T2 释放 O2 的锁.
  • 但是 T2 这时会去请求 O1 的锁, O1 的锁被 T1 占有, 所以 T2 也进入阻塞状态, 等待 T1 释放 O1 的锁.

两个线程同时等待对方释放对象锁, 又同时占有对方需要的对象锁. 于是就进入阻塞状态.

代码如下


private static String O1 = "O1";
    private static String O2 = "O2";

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();

        executorService.submit(
                () -> {
                    synchronized (O1) {
                        try {
                            TimeUnit.SECONDS.sleep(2);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        synchronized (O2) {
                            System.out.println(O1);
                            System.out.println(O2);
                        }
                    }
                }
        );

        executorService.submit(
                () -> {
                    synchronized (O2) {
                        try {
                            TimeUnit.SECONDS.sleep(2);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        synchronized (O1) {
                            System.out.println(O2);
                            System.out.println(O1);
                        }
                    }
                }
        );
        
        executorService.shutdown();
    }


执行这段代码会发现程序不会自动停止, 并且控制台没有打印任何东西.

如何避免死锁

控制加锁顺序

上边的死锁实例代码中, 第一个线程和第二个线程中对 O1 和 O2 的加锁顺序是不同的(第一个线程先获得 O1 再获得 O2, 第二个线程相反). 这是到时死锁的重要原因之一.

所以, 如果所有线程都使用相同的加锁顺序. 以上代码就可以避免死锁, 比如这样

public static void test2 () {
        ExecutorService executorService = Executors.newCachedThreadPool();

        executorService.submit(
                () -> {
                    synchronized (O1) {
                        try {
                            TimeUnit.SECONDS.sleep(2);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        synchronized (O2) {
                            System.out.println(O1);
                            System.out.println(O2);
                        }
                    }
                }
        );

        executorService.submit(
                () -> {
                    synchronized (O1) {
                        try {
                            TimeUnit.SECONDS.sleep(2);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        synchronized (O2) {
                            System.out.println(O2);
                            System.out.println(O1);
                        }
                    }
                }
        );

        executorService.shutdown();
    }

以上代码会打印出正确的结果, 并且执行完后自动退出. 并没有死锁.

O1
O2
O2
O1

Process finished with exit code 0

但是在一些开发周期长, 开发人说多的大型项目中, 很难控制枷锁顺序.

给对象锁设置时间限制

也就是说每个线程占有对象锁的时间不能超过设置的最大时间. 如果超过, 对象锁将会被强制释放.

  • 建议设置一个比较大的时间限制, 避免在线程操作共享对象过程中锁被释放.
  • 在超过最大时间后建议打印一些 log, 避免和其他中断情况混淆, 难以查询 bug.

活锁 Livelock

活锁同死锁一样会使程序进入停止状态, 无法继续执行. 线程之间相互谦让,都主动将资源释放给别的线程使用,这样这个资源在多个线程之间跳动而又得不到执行,这就是活锁。

触发条件

  • 多线线程同时访问同一个资源
  • 当资源有其他线程访问时, 当前线程会主动释放资源

以下情况就属于活锁.

假设有两个线程 T1 和 T2 需要同时获得共享对象 O.

  • 程序开始执行后, T1 先得到了 O 的对象锁, 开始对 O 进行操作
  • 这时 T2 也去请求 O. 当 T1 发现 T2 在请求 O 时, 会主动中断自己, 让出 O 的锁, 然后进入待执行状态, 等待 T2 释放 O 的锁
  • T2 得到 O 后立即发现 T1 在待执行状态等待 O 的锁, 于是 T2 没有继续执行, 立即中断, 释放 O. 后进入待执行状态, 等待 T1 释放 O 的锁
  • T1 和 T2 重复以上动作, 进入无限循环

饥饿 Starvation

饥饿就是有些线程永远无法的到需要的资源而永远处于阻塞状态.

JVM的多线程环境中, 优先级的不确定性也是为了避免饥饿问题.

触发条件

  • 优先级高的线程总会先执行.

以下情况必然发生饥饿

系统中总是同时存在高优先级和低优先级的线程

  • 一个高优先级的线程得到资源, 然后执行, 当资源被释放后又被另一个高优先级线程抢占.
  • 系统会不停的产生高优先级的线程
  • 低优先级线程永远无法得到资源, 永远处于阻塞状态

无锁

在死锁, 活锁, 饥饿中我提到的资源(或共享资源) 指的都是实现了同步锁的资源, 也就是说线程只有拿到对象锁, 才能操作资源, 并且同一时间只能有一个线程得到对象锁.

无锁就是多线程访问没有实现上边所说的同步锁的资源. 也就是说无锁是不考虑同步情况的. 那么我们为什么要考虑同步情况能.

没有实现同步而引发的问题

假如有一个没有实现同步的List 对象, 它的初始值为 null, 第一次使用时需要初始化, 然后多个线程需要向 List 里添加值.

List<String> list = null;

所有线程都会执行以下操作

1> if (list == null) 
2>     list = new ArrayList<>();
3> list.add("thread name");
  • 线程 T1 执行了 1>, 2>, 3>
  • 线程 T2 在线程 T1 执行了 1> 但还没执行 2> 时, 执行了 1>, 所以 T2 线程认为 list 仍然为空, 所以它也去执行 2>
  • 线程 T2 在执行并完成 2> 之前, 线程 T1 就已经完成了 2> 和 3>, 也就是给 list 增加了一个string
  • 线程 T2 执行完成 2> 给 list 赋值了自己创建的新 list 对象,
  • 线程 T2 给 list 中增加了一个 string

当程序执行完成后, 我们期望的结果是 list 中有2个 string 对象. 但如果按照上述步骤执行, list实际上只有一个 string

避免死锁 - Java 并发性和多线程 - 极客学院Wiki
Deadlock Prevention
死锁产生的4个必要条件,如何检测,解除死锁。请高手解答。_百度知道
Starvation and Livelock (The Java™ Tutorials > Essential Classes > Concurrency)
java多线程中的死锁、活锁、饥饿、无锁都是什么鬼? - 简书