线程的活跃性问题

前面我们了解了多线程带来的安全性问题,接下来我们来看下多线程带来的活跃性问题。

一、活跃性问题

线程是为任务而生的,理想情况下,我们希望线程能一直处于运行(Runnable)状态,但是会由于一些因素,如处理器资源有限导致的上下文切换、程序自身的错误和缺陷。这些由于资源稀缺或者程序自身问题导致线程无法一直处于 Runnable 状态运行下去,又或者因为线程处于 Runnable 状态但是其要执行的任务一直无法进展的现象就被称为线程活跃性问题活性故障。我们常见的“死锁”就是一种典型的活跃性问题,除了“死锁”,还有“活锁”和“饥饿”。

二、死锁

1.什么是死锁

死锁是多线程中一种常见的活性故障。如果在并发情况中多线程因相互等待对方而被永远暂停,那么我们就称这些线程产生了死锁。死锁产生的一种典型情况是线程 A 在持有锁 L1 的情况下去申请锁 L2,线程 B 在持有锁 L2 的情况下去申请锁 L1,线程 A 只有在获得并释放锁 L2 后才会释放锁 L1,线程 B 只有在获得并释放锁 L1 后才会释放锁 L2。此时,线程 A 和线程 B 各自都在持有一个锁的情况下去申请对方持有的锁,而线程 A 和线程 B 释放其持有锁的前提又都是先获得对方持有的另一个锁,此时两个线程就处于无限等待的状态,即产生了死锁,如下图所示。

2.死锁的案例

2.1 问题描述

死锁相关的一个经典问题就是哲学家就餐问题,5 个哲学家(顺时针:亚里士多德、柏拉图、苏格拉底、伏尔泰、笛卡尔)相约去聚餐,哲学家们围坐在一个圆桌上,每个座位前都有一个碗,装了满满的面,每个碗之间都有一个叉子。就餐的时候,每个哲学家总是先拿起自己左手边的叉子,再拿起自己右手边的叉子,只有手上持有两个叉子时才能够就餐。哲学家吃着吃着就会放下手中的叉子进行思考,思考完后又继续就餐,如此的在吃面和思考之间反复。如下图所示。

简易的步骤如下所示:

  • 先拿左边的叉子
  • 然后拿右边的叉子
  • 如果有人在用叉子,那就等别人用完
  • 吃完后,将叉子放回原位

伪代码如下所示:

while (true) {
	think();
	pick_up_left_fork();
	pick_up_right_fork();
	eat();
	put_down_right_fork();
	put_down_left_fork();
}

2.2 死锁场景

如果把每个哲学家都看作一个线程(哲学家线程),那么由于叉子的数量等于哲学家的数量而不是哲学家数量的两倍,因此叉子可以被看作线程间的共享资源。一个叉子由于无法同时被两个哲学家使用,因此哲学家线程在访问这些资源的时候就需要加锁。由于每个哲学家右手边的叉子正是其右手边哲学家左手边的叉子,因此在 5 个哲学家同时开始吃饭的情况下可能出现每个哲学家刚刚拿起其左手边的叉子之后准备拿起右手边的叉子时,右手边的叉子恰好已经被右手边的哲学家拿起。于是每个哲学家都是左手拿着叉子而在等待其右手边的哲学家放下其左手中的叉子,因此这种情形最终演变成任何一个哲学家实质上都是在等待自己放下其左手的叉子才能拿到其右手的叉子,而每个哲学家放下其左手的叉子的前提又是先拿到其右手边的叉子,此时就会产生死锁情况。

2.3 代码演示

/**
 * @author hncboy
 * @description 演示哲学家就餐问题导致的死锁
 */
public class DiningPhilosophers {

    public static void main(String[] args) {
        // 定义 5 个哲学家线程和对应数量的叉子
        Philosopher[] philosophers = new Philosopher[5];
        Object[] forks = new Object[philosophers.length];

        for (int i = 0; i < forks.length; i++) {
            forks[i] = new Object();
        }

        // 遍历哲学家线程进行操作
        for (int i = 0; i < philosophers.length; i++) {
            // 左边的叉子
            Object leftFork = forks[i];
            // 右边的叉子
            Object rightFork = forks[(i + 1) % forks.length];

            philosophers[i] = new Philosopher(leftFork, rightFork);
            new Thread(philosophers[i], "Philosopher " + (i + 1) + "").start();
        }
    }

    /**
     * 哲学家线程
     */
    public static class Philosopher implements Runnable {

        /**
         * 左手的叉子
         */
        private final Object leftFork;

        /**
         * 右手的叉子
         */
        private final Object rightFork;

        public Philosopher(Object leftFork, Object rightFork) {
            this.leftFork = leftFork;
            this.rightFork = rightFork;
        }

        @Override
        public void run() {
            while (true) {
                // 思考
                doAction("Thinking");
                synchronized (leftFork) {
                    // 拿到左手的叉子
                    doAction("Picked up left fork");
                    synchronized (rightFork) {
                        // 拿到右手的叉子
                        doAction("Pick up right fork");
                        doAction("Eating");
                        doAction("Put down right fork");
                    }
                    doAction("Put down left fork");
                }
            }
        }

        private void doAction(String action) {
            System.out.println(Thread.currentThread().getName() + " " + action);
            try {
                // 随机睡眠时间
                Thread.sleep((long) (Math.random() * 10));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

2.4 定位死锁

运行上面这段代码我们可以发现,不用多久程序就停止运行了,那么我们怎么定位死锁呢。首先我们通过 jps 命令查找 java 相关进程,然后通过 jstack pid 命令查看对应进程的线程转储。

可以发现有一个死锁,如下图所示。

这个死锁包含了 5 个线程,形成表格如下所示,不难发现,每个哲学家线程都是在持有一个锁的情况下去申请上一个哲学家线程持有的另外一个锁,造成了死锁问题。

线程名持有的锁申请的锁
Philosopher 10x0000000780953f480x0000000780953f58
Philosopher 20x0000000780953f580x0000000780953f68
Philosopher 30x0000000780953f680x0000000780953f78
Philosopher 40x0000000780953f780x0000000780953f88
Philosopher 50x0000000780953f880x0000000780953f48

3.死锁的产生

哲学家就餐问题反映了产生死锁的必要条件,线程一旦产生死锁,那么这些线程及相关的资源满足如下条件,缺一不可。

  • 互斥条件:涉及的共享资源必须是独占的,即每一个资源一次只能被一个线程所使用。例如,哲学家就餐问题中的叉子(或者使用叉子时所需持有的锁)可被看作独占的条件。
  • 循环等待条件:涉及的线程必须在等待别的线程持有的资源,而这些线程反过来在等待第 1 个线程所持有的资源。例如,哲学家就餐问题中第一个哲学家在等待第二个哲学家左手持有的叉子,第二个哲学家在等待第三个哲学家左手持有的叉子,…,第五个哲学家在等待第一个哲学家左手持有的叉子。
  • 不可抢占条件:涉及的资源只能够被其持有者线程主动释放,其他线程无法抢占该资源。例如,哲学家就餐问题中的叉子只能由持有该叉子的哲学家线程主动释放。
  • 请求与保持条件:涉及的线程当前至少持有一个资源并且申请其他资源,而其他资源刚好被其他线程所占有。在这个请求其他资源的过程中,线程并不会释放其已持有的资源。例如,哲学家就餐问题中一个哲学家左手拿着叉子而等待其右手边的叉子,右手边的叉子恰好被其右手边的哲学家所持有。并且,等待其他哲学家手上叉子的过程中并不会释放自己手中的叉子。

这些条件是死锁产生的必要条件而非充分条件,也就是说只要产生了死锁,上面的 4 个条件一定同时成立,但是上述条件成立的情况下也不一定会发生死锁。因此,死锁问题和其他多线程相关问题(可见性问题)类似,并不是必然发生的。上述几个条件并非完全独立,其中“循环等待条件”可能包含“请求与保持条件”,而“请求与保持条件”是“循环等待条件”的基础,但不意味着“循环等待条件”。

4.死锁的规避

弄清死锁产生的必要条件也就不难想到规避死锁的方法,也就是说我们只要破坏其中一个死锁产生的条件,就可以避免死锁的发生。由于锁本身就具有排他性,因此互斥条件我们无法进行破坏,不过可以破坏其他三个条件。

4.1 破坏循环等待条件

锁排序法—相关线程使用全局统一的顺序(hashcode、主键等)申请锁。假设有多个线程需要申请锁,那么我们只需要让这些线程按照一定的顺序去申请这些锁,就可以破坏“循环等待条件”,从而避免死锁。例如,在哲学家就餐问题中每个哲学家都是按照“先拿左手边叉子,再拿右手边叉子”这种局部顺序来拿叉子的。这种顺序为“局部”,是因为一个哲学家右手边的叉子恰恰是另一个哲学家左手边的叉子。因此,从全局的角度去看这种拿叉子的顺序是各个哲学家线程独立的顺序,从而使得“循环等待条件”成立。为了破坏“循环等待条件”这个死锁产生的必要条件,我们可以让所有哲学家线程采用一种全局统一的顺序去拿叉子,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后自然就不存在循环了,这种方法实际上是对资源(叉子)进行排序。

4.2 破坏不可抢占条件

破坏“不可抢占条件”的核心时当占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样“不可抢占条件”就被破坏了。这种破坏条件使用 synchronized 是做不到的,当使用 synchronized 申请资源时,申请不到的线程就直接进入阻塞状态了,也释放不了线程已经占有的资源。我们可以使用 ReentrantLock 类中 tryLock(long timeout, TimeUnit unit) 方法为锁申请资源指定一个超时时间,在超时时间内,如果相应的锁申请成功,那么该方法就返回 true。如果在 tryLock(long timeout, TimeUnit unit) 执行的时候,相应的锁正在被其他线程持有,那么该方法会使当前线程等待,直到锁申请成功或者等待时间超过超时时间(返回 false)。因此,使用该方法可以有效的避免“不可抢占条件”的发生。

4.3 破坏请求与保持条件

锁粗法—使用粗粒度的锁代替多个锁。从消除“请求与保持条件”出发我们不难想到的一种方法是,采用一个粒度较粗的锁来代替原来的多个粒度较细的锁,这样涉及的线程都只需要申请一个锁从而避免了死锁。按照这个思路,针对哲学家就餐问题,我们可以定义一个较大范围的锁,当任何一个哲学家线程拿到这个锁(叉子)时,其他哲学家线程就进入等待状态。此时,由于每个哲学家线程仅需要一个锁就可以就餐,因此死锁产生的必要条件“请求与保持条件”和“循环等待条件”就不成立了,从而避免了死锁。

锁粗法的缺点是它明显降低了并发性并可能导致资源浪费。例如,哲学家就餐采用锁粗法的结果是一次只能够一个哲学家就餐,一个哲学家在就餐的时候其他哲学家只能在思考或者等待叉子。而实际上,一个哲学家就餐只占用了 2 个叉子,剩下的 3 个叉子还可以给一个哲学家使用。因此,锁粗法的适用范围比较有限。

4.4 其他方法

规避死锁最好的方法其实就是不使用锁,无锁化编程。我们可以使用一些锁的替代品(线程本地对象 ThreadLocal、volatile 关键字、CAS 等)。在条件允许的情况下使用这些替代品在保障线程安全的前提下不仅能避免使用锁的开销,还能够直接避免死锁。

4.5 代码演示

演示采用 Lock 的 tryLock(long time, TimeUnit unit) 方法来避免死锁,两个线程分别依次申请锁 LOCK1、LOCK2 和 LOCK2、LOCK1,引入随机时间,代码如下。

/**
 * @author hncboy
 * @description 用 tryLock 来避免死锁
 */
public class TryLockDeadLock implements Runnable {

    private boolean flag;
    private static final Lock LOCK1 = new ReentrantLock();
    private static final Lock LOCK2 = new ReentrantLock();

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            try {
                if (flag) {
                    if (LOCK1.tryLock(800, TimeUnit.MILLISECONDS)) {
                        System.out.println(Thread.currentThread().getName() + " 获取到 LOCK1");
                        Thread.sleep(new Random().nextInt(1000));
                        if (LOCK2.tryLock(800, TimeUnit.MILLISECONDS)) {
                            System.out.println(Thread.currentThread().getName() + " 获取到 LOCK2,成功获取到两把锁");
                            LOCK2.unlock();
                            LOCK1.unlock();
                            break;
                        } else {
                            System.out.println(Thread.currentThread().getName() + " 获取 LOCK2 失败,已重试");
                            LOCK1.unlock();
                        }
                    } else {
                        System.out.println(Thread.currentThread().getName() + " 获取 LOCK1 失败,已重试");
                        Thread.sleep(new Random().nextInt(1000));
                    }
                } else {
                    if (LOCK2.tryLock(3000, TimeUnit.MILLISECONDS)) {
                        System.out.println(Thread.currentThread().getName() + " 获取到 LOCK2");
                        Thread.sleep(new Random().nextInt(1000));
                        if (LOCK1.tryLock(3000, TimeUnit.MILLISECONDS)) {
                            System.out.println(Thread.currentThread().getName() + " 获取到 LOCK1,成功获取到两把锁");
                            LOCK1.unlock();
                            LOCK2.unlock();
                            break;
                        } else {
                            System.out.println(Thread.currentThread().getName() + " 获取 LOCK1 失败,已重试");
                            LOCK2.unlock();
                        }
                    } else {
                        System.out.println(Thread.currentThread().getName() + " 获取 LOCK2 失败,已重试");
                        Thread.sleep(new Random().nextInt(1000));
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        TryLockDeadLock tryLockDeadLock1 = new TryLockDeadLock();
        TryLockDeadLock tryLockDeadLock2 = new TryLockDeadLock();
        tryLockDeadLock1.flag = true;
        tryLockDeadLock2.flag = false;

        new Thread(tryLockDeadLock1, "Thread1").start();
        new Thread(tryLockDeadLock2, "Thread2").start();
    }
}

运行结果如下

Thread2 获取到 LOCK2
Thread1 获取到 LOCK1
Thread1 获取 LOCK2 失败,已重试
Thread1 获取到 LOCK1
Thread1 获取 LOCK2 失败,已重试
Thread2 获取到 LOCK1,成功获取到两把锁
Thread1 获取到 LOCK1
Thread1 获取到 LOCK2,成功获取到两把锁

5.死锁的恢复

死锁未产生时我们可以采用规避方法,那一旦产生死锁,我们则需要对死锁的线程进行恢复。

5.1 恢复方法

如果代码中使用隐式锁或者使用显式锁时采用 Lock.lock() 的方式调用,那么这些锁使用所导致的死锁是不可恢复的,需要重启 JVM 进程才可以终止。如果代码中使用显式锁且锁的申请采用 Lock.lockInterruptibly() 调用的方式实现的,那么这些锁导致的死锁问题理论上是可以修复的。但是进行恢复的可操作性也不强:故障线程可能无法响应中断或者中断操作可能导致其他线程活性故障

死锁的恢复有赖于线程的中断机制,我们可以定义一个工作线程去专门检测死锁和并且恢复。该线程定期检测系统中是否存在死锁,若检测到死锁,则随机选取一个死锁线程进行中断命令的调用。该中断使得一个任意的死锁线程被 JVM 唤醒,从而抛出 InterruptedException 异常。使得目标线程不再等待它本来永远也无法申请到的资源,从而破坏了死锁产生必要条件中的“请求与保持条件”中的“请求”资源部分。目标线程通过处理 InterruptedException 异常的方式来响应中断,目标线程在捕获到 InterruptedException 异常后主动释放其持有的锁,这相当于破环了死锁产生必要条件中“请求与保持条件”的“保持”资源部分。进行完上面一次操作后,死锁检测工作线程会继续检测系统中是否仍然存在死锁,若存在,则继续选择任意一个死锁线程并向其发送中断,直到系统中不存在死锁。

5.2 代码演示

一个简易的用于检测死锁并且中断死锁线程的代码如下,其中用了 ThreadMXBean 的 findDeadlockedThreads() 方法查询造成死锁的线程。

/**
 * @author hncboy
 * @description 用 ThreadMXBean 检测死锁
 */
public class DeadLockDetector implements Runnable {

    private boolean flag;
    private static final ReentrantLock LOCK1 = new ReentrantLock();
    private static final ReentrantLock LOCK2 = new ReentrantLock();

    @Override
    public void run() {
        if (flag) {
            try {
                LOCK1.lockInterruptibly();
                System.out.println(Thread.currentThread().getName() + " 获取到 LOCK1");
                Thread.sleep(new Random().nextInt(1000));
                LOCK2.lockInterruptibly();
                System.out.println(Thread.currentThread().getName() + " 获取到 LOCK2,成功获取到两把锁");
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName() + " 被中断");
            } finally {
                if (LOCK1.isHeldByCurrentThread()) {
                    LOCK1.unlock();
                }
                if (LOCK2.isHeldByCurrentThread()) {
                    LOCK2.unlock();
                }
            }
        } else {
            try {
                LOCK2.lockInterruptibly();
                System.out.println(Thread.currentThread().getName() + " 获取到 LOCK2");
                Thread.sleep(new Random().nextInt(1000));
                LOCK1.lockInterruptibly();
                System.out.println(Thread.currentThread().getName() + " 获取到 LOCK1,成功获取到两把锁");
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName() + " 被中断");
            } finally {
                if (LOCK1.isHeldByCurrentThread()) {
                    LOCK1.unlock();
                }
                if (LOCK2.isHeldByCurrentThread()) {
                    LOCK2.unlock();
                }
            }
        }

    }

    /**
     * 根据 threadId 获取对应的线程
     *
     * @param threadId
     * @return
     */
    private static Thread findThreadById(long threadId) {
        for (Thread thread : Thread.getAllStackTraces().keySet()) {
            if (thread.getId() == threadId) {
                return thread;
            }
        }
        return null;
    }

    public static void main(String[] args) throws InterruptedException {
        DeadLockDetector deadLockDetector1 = new DeadLockDetector();
        DeadLockDetector deadLockDetector2 = new DeadLockDetector();
        deadLockDetector1.flag = true;
        deadLockDetector2.flag = false;

        new Thread(deadLockDetector1, "Thread1").start();
        new Thread(deadLockDetector2, "Thread2").start();

        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        // 获取死锁的线程
        while (true) {
            Thread.sleep(500);
            long[] deadLockedThreads = threadMXBean.findDeadlockedThreads();
            if (deadLockedThreads != null && deadLockedThreads.length > 0) {
                System.out.println("检测到死锁");
                for (int i = 0; i < deadLockedThreads.length; i++) {
                    ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadLockedThreads[i]);
                    System.out.println("死锁线程:" + threadInfo.getThreadName());
                    // 中断死锁
                    Thread thread = findThreadById(threadInfo.getThreadId());
                    thread.interrupt();
                    break;
                }
            }
        }
    }
}

运行结果如下:

Thread1 获取到 LOCK1
Thread2 获取到 LOCK2
检测到死锁
死锁线程:Thread2
Thread2 被中断
Thread1 获取到 LOCK2,成功获取到两把锁

6.死锁的检测

  • 手动查看线程转储(Thread Dump)检测死锁(jstack)
  • 利用 ThreadMXBean 的 findDeadlockedThreads() 方法检测死锁

这两个方法的使用在死锁的案例死锁的恢复中都有涉及到。

7.实际工程避免死锁

  • 设置锁超时时间: Lock 的 tryLock(long timeout, TimeUnit unit)
  • 多采用并发类而不是自己设计锁:ConcurrentXxx、AtomicXxx 等
  • 尽量降低锁的使用粒度:使用不同的锁而不是同一个锁
  • 如果能使用同步代码块,就不使用同步方法:自己指定锁对象
  • 给线程指定一个有意义的名字:方便 debug 和排查问题
  • 避免锁的嵌套使用
  • 分配资源之前先看看能不能回收回来:银行家算法
  • 专锁专用:尽量不要多个功能使用同一个把锁

三、饥饿

1.什么是饥饿

线程饥饿是指线程一直无法获得其所需的资源而导致其任务一直无法进展的一种活性故障。线程饥饿相当于俗话说的“巧妇(线程)难为无米(资源)之炊(任务)”。“不患寡,而患不均”,如果线程优先级分配“不均匀”,在 CPU 繁忙的情况下,优先级低的线程得到执行的机会很小,就可能发生线程“饥饿”;持有锁的线程,如果执行的时间过长,也可能导致“饥饿”问题。

2.饥饿的案例

线程饥饿的一个典型案例是高争用的情况下使用非公平模式的读写锁。例如,在程序中使用 ReentrantReadWriteLock 来保护配置数据,业务线程可能不断地申请读锁来读取配置文件,由于 ReentrantReadWriteLock 读写锁默认采用非公平的调度模式进行锁的调度,如果这些业务线程对读锁的争用程度较高,可能导致系统管理模块试图更新配置数据时一直获取不到相应的写锁,从而一直无法更新配置数据。因此,尽管非公平锁支持较高的吞吐率,但是可能导致某些线程无法获取到其所需的资源,导致线程饥饿。

线程饥饿涉及的线程,对应的生命周期状态不一定为 WAITING 或 BLOCKED,也可能是 RUNNABLE(说明对应的线程一直在申请其所需的资源),此时饥饿就变为了活锁。

3.饥饿的影响

把锁看作是一种资源,可以发现死锁也是一种线程饥饿。但是由于线程饥饿的产生条件是一个或者多个线程始终无法获取到其所需的资源,这个条件并不满足死锁产生的必要条件,因此线程饥饿并不会导致死锁。

饥饿可能导致响应性差,比如:浏览器有一个线程负责处理前台响应(打开收藏夹等动作),其他的后台线程负责下载图片文件、渲染数据等。在这种情况下,如果后台线程将 CPU 资源都占用了,则前台线程将无法很好的执行,使得用户的体验较差。

4.饥饿的解决

  • 保证资源充足
  • 公平的分配资源:使用公平锁
  • 避免持有锁的线程长时间执行
  • 避免设置线程优先级:不过设置了用处也不大,优先级会被操作系统改变,不同操作系统也不一样

四、活锁

1.什么是活锁

活锁是指线程一直处于运行状态,但是其任务一直无法进展的一种活性故障。产生活锁的线程一直在做无用功,如小猫一直追着自己的尾巴咬,虽然一直在咬,但是一直咬不到自己的尾巴。线程在争取其所需的资源过程中如果“屡战屡败,屡败屡战”,线程一直在申请其所需的资源而一直未申请成功,此时线程饥饿实际上就演变成活锁。

2.活锁的案例

一个类似哲学家就餐的案例。有两个就餐者,他们都是饥饿状态,准备就餐,但是只有一个勺子。当一个人拥有勺子的时候,他首先会观察另一个就餐者是否处于饥饿状态,是的话,将勺子给另一个就餐者。如此反复,两个就餐者线程一直在运行,这就是活锁,代码如下。

/**
 * @author hncboy
 * @description 演示活锁问题
 */
public class LiveLock {

    public static void main(String[] args) {
        Diner diner1 = new Diner("diner1");
        Diner diner2 = new Diner("diner2");
        Spoon spoon = new Spoon(diner1);

        new Thread(() -> diner1.eatWith(spoon, diner2)).start();
        new Thread(() -> diner2.eatWith(spoon, diner1)).start();

    }

    /**
     * 勺子
     */
    private static class Spoon {

        private Diner owner;

        public Spoon(Diner owner) {
            this.owner = owner;
        }

        public synchronized void use() {
            System.out.printf("%s 使用勺子!", owner.name);
        }

        public void setOwner(Diner owner) {
            this.owner = owner;
        }
    }

    /**
     * 就餐者
     */
    private static class Diner {

        private final String name;
        private boolean isHungry = true;

        public Diner(String name) {
            this.name = name;
        }

        /**
         * 吃饭
         *
         * @param spoon  勺子
         * @param otherDiner 另一个用餐者
         */
        public void eatWith(Spoon spoon, Diner otherDiner) {
            while (isHungry) {
                // 如果勺子不在自己这里,继续循环
                if (spoon.owner != this) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    continue;
                }

                // 如果勺子在自己手中,但另一个用餐者是饥饿的,将勺子交给另外一个用餐者
                if (otherDiner.isHungry) {
                    System.out.println(name + " 将勺子给 " + otherDiner.name);
                    spoon.setOwner(otherDiner);
                    continue;
                }

                // 如果勺子在自己手中,而且另一个就餐者也不饿,自己使用勺子,设置不饿状态,将勺子交给另一个就餐者
                spoon.use();
                isHungry = false;
                System.out.println(name + ":我吃完了");
                spoon.setOwner(otherDiner);
            }
        }
    }
}

运行结果如下,会进行无限循环。

diner1 将勺子给 diner2
diner2 将勺子给 diner1
diner1 将勺子给 diner2
diner2 将勺子给 diner1
......

3.活锁的解决

上面案例活锁产生原因是重试机制不变,一直在重试,我们可以引入随机因素来改变重试时间。部分改动代码如下:

/**
 * @author hncboy
 * @description 演示活锁问题的解决
 */
public class SolutionLiveLock {

    ......
        
    private static class Diner {

        ......
            
        public void eatWith(Spoon spoon, Diner otherDiner) {
            while (isHungry) {
                ......

                // 如果勺子在自己手中,但另一个用餐者是饥饿的,将勺子交给另外一个用餐者
                Random random = new Random();
                // 引入随机时间
                if (otherDiner.isHungry && random.nextInt(10) < 9) {
                    System.out.println(name + " 将勺子给 " + otherDiner.name);
                    spoon.setOwner(otherDiner);
                    continue;
                }

                ......
            }
        }
    }
}

我们在判断另一个就餐者是否饥饿时,引入随机值,运行结果如下所示。

diner1 将勺子给 diner2
diner2 将勺子给 diner1
diner1 将勺子给 diner2
diner2 将勺子给 diner1
diner1 使用勺子!diner1:我吃完了
diner2 使用勺子!diner2:我吃完了

“等待一个随机时间”的方案虽然很简单,却非常有效,Raft 这样知名的分布式一致性算法中也用到了它。

五、总结

  • 死锁是相关线程一直被暂停导致任务无法进展。
  • 饥饿是指线程一直无法获得其所需的资源而导致其任务一直无法进展的一种活性故障。
  • 活锁是指线程一直在做无用功而使其任务一直无法进展的一种活性故障。

参考资料

《Java 多线程编程实战指南(核心篇)》

极客时间《Java 并发编程实战》

慕课网《线程八大核心+Java并发底层原理精讲》

灿烂一生
微信扫描二维码,关注我的公众号
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值