第十三章、 并发死锁问题与企业级解决方案(死锁、活锁、饥饿)

1、死锁是什么?有什么危害?

1.1 什么是死锁?

  • 发生在【并发】中
  • 【互不相让】:当两个(或更多)线程(或进程)相互持有对方所需要的资源,又不主动释放,导致所有人都无法继续前进,导致程序陷入无尽的阻塞,这就是死锁。 
  • 多个线程造成死锁的情况(A->B->C->A)

1.2 死锁的影响

死锁的影响在不同系统中是不一样的,这取决于系统对死锁的处理能力

  • 数据库中:检测到死锁,(两个事务AB相互竞争),会放弃其中一个事务A,让B先执行,然后再执行A
  • JVM中:无法自动处理

1.3 机率不高但危害大

  • 不一定发生,但遵循“墨菲定律”(如果事情有变坏的可能,不管这种可能性有多小,它总会发生)
  • 一旦发生,多是【高并发】场景,影响用户多
  • 整个系统崩溃、子系统崩溃、性能降低
  • 压力测试无法找出所有潜在的死锁

2、发生死锁的例子

2.1 最简单的情况

代码

/**
 * MustDeadLock
 *
 * @author venlenter
 * @Description: 必定发生死锁的情况
 * @since unknown, 2020-06-10
 */
public class MustDeadLock implements Runnable {
    int flag = 1;
    static Object o1 = new Object();
    static Object o2 = new Object();

    public static void main(String[] args) {
        MustDeadLock r1 = new MustDeadLock();
        MustDeadLock r2 = new MustDeadLock();
        r1.flag = 1;
        r2.flag = 0;
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);
        t1.start();
        t2.start();
    }
    @Override
    public void run() {
        System.out.println("flag = " + flag);
        if (flag == 1) {
            synchronized (o1) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o2) {
                    System.out.println("线程1成功拿到两把锁");
                }
            }
        }
        if (flag == 0) {
            synchronized (o2) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o1) {
                    System.out.println("线程2成功拿到两把锁");
                }
            }
        }
    }
}

//输出结果
flag = 1
flag = 0
//线程一直不解释,处于死锁状态

分析

  • T1和T2【互相等待】,都需要对方锁定的资源才能继续执行,从而死锁
  • 强制中止程序,IDEA会打印多一行(code -1)
flag = 1
flag = 0

Process finished with exit code -1
  • 非0是不正常退出的信号,正常结束的程序的【结束信号是0】

2.2 实际生产中的例子:转账

  • 需要两把锁
  • 获取两把锁成功,且余额大于0,则扣除转出人,增加收款人的余额,是原子操作
  • 顺序相反导致死锁
/**
 * TransferMoney
 *
 * @author venlenter
 * @Description: 转账时候遇到死锁,一旦打开注释,便会发生死锁
 * @since unknown, 2020-06-13
 */
public class TransferMoney implements Runnable {
    int flag = 1;
    static Account a = new Account(500);
    static Account b = new Account(500);

    public static void main(String[] args) throws InterruptedException {
        TransferMoney r1 = new TransferMoney();
        TransferMoney r2 = new TransferMoney();
        r1.flag = 1;
        r2.flag = 0;
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("a的余额" + a.balance);
        System.out.println("b的余额" + b.balance);
    }

    @Override
    public void run() {
        if (flag == 1) {
            transferMoney(a, b, 200);
        }
        if (flag == 0) {
            transferMoney(b, a, 200);
        }
    }

    public static void transferMoney(Account from, Account to, int amount) {
        synchronized (from) {
//            try {
//                Thread.sleep(500);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
            synchronized (to) {
                if (from.balance - amount < 0) {
                    System.out.println("余额不足,转账失败");
                }
                from.balance -= amount;
                to.balance += amount;
                System.out.println("成功转账" + amount + "元");
            }
        }
    }

    static class Account {
        public Account(int balance) {
            this.balance = balance;
        }

        int balance;
    }
}

//输出结果(没有死锁的情况)
成功转账200元
成功转账200元
a的余额500
b的余额500

//开启备注的Thread.sleep则a和b线程死锁,没有输出,都在相互等待

2.3 模拟多人随机转账

  • 5W人很多,但是依然会发生死锁,墨菲定律
  • 发生死锁机率不高但危害大
/**
 * MultiTransferMoney
 *
 * @author venlenter
 * @Description: 多人同时转账,依然很危险
 * @since unknown, 2020-06-13
 */
public class MultiTransferMoney {
    private static final int NUM_ACCOUNTS = 500;
    private static final int NUM_MONEY = 1000;
    private static final int NUM_THREADS = 20;
    private static int NUM_ITERATIONS = 1000000;

    public static void main(String[] args) {
        Random rnd = new Random();
        Account[] accounts = new Account[NUM_ACCOUNTS];
        for (int i = 0; i < accounts.length; i++) {
            accounts[i] = new Account(NUM_MONEY);
        }
        class TransferThread extends Thread {
            @Override
            public void run() {
                for (int i = 0; i < NUM_ITERATIONS; i++) {
                    int fromAcct = rnd.nextInt(NUM_ACCOUNTS);
                    int toAcct = rnd.nextInt(NUM_ACCOUNTS);
                    int amount = rnd.nextInt(NUM_MONEY);
                    TransferMoney.transferMoney(accounts[fromAcct], accounts[toAcct], amount);
                }
                System.out.println("运行结束");
            }
        }
        for (int i = 0; i < NUM_THREADS; i++) {
            new TransferThread().start();
        }
    }
}
//输出结果(输出到一定时间后,20个线程都卡住了,死锁)
成功转账568元
成功转账129元
成功转账225元
成功转账623元
...
成功转账889元
余额不足,转账失败
成功转账451元
余额不足,转账失败
成功转账138元
//所有线程卡住

3、死锁的4个必要条件(缺一不可)

  • 互斥条件(线程A拿到了锁lock-a,则其他线程要获取lock-a时只能等待)
  • 请求与保持条件(线程A在请求lock-b的时候,同时保持着lock-a锁)
  • 不剥夺条件(线程A持有lock-a,外界不能剥夺A对lock-a的持有)
  • 循环等待条件(多个线程形成环路,A等待B,B等待C,C等待A)

4、如何定位死锁

4.1 使用java命令jstack(${JAVA_HOME}/bin/jstack pid)

  • 例子
1 package ConcurrenceFolder.mooc.threadConcurrencyCore.deadlock;
2 
3 /**
4 * MustDeadLock
5 *
6 * @author venlenter
7 * @Description: 必定发生死锁的情况
8 * @since unknown, 2020-06-10
9 */
10 public class MustDeadLock implements Runnable {
11    int flag = 1;
12    static Object o1 = new Object();
13    static Object o2 = new Object();
14
15    public static void main(String[] args) {
16        MustDeadLock r1 = new MustDeadLock();
17        MustDeadLock r2 = new MustDeadLock();
18        r1.flag = 1;
19        r2.flag = 0;
20        Thread t1 = new Thread(r1);
21        Thread t2 = new Thread(r2);
22        t1.start();
23        t2.start();
24    }
25    @Override
26    public void run() {
27        System.out.println("flag = " + flag);
28        if (flag == 1) {
29            synchronized (o1) {
30                try {
31                    Thread.sleep(500);
32                } catch (InterruptedException e) {
33                    e.printStackTrace();
34                }
35                synchronized (o2) {
36                    System.out.println("线程1成功拿到两把锁");
37                }
38            }
39        }
40        if (flag == 0) {
41            synchronized (o2) {
42                try {
43                    Thread.sleep(500);
44                } catch (InterruptedException e) {
45                    e.printStackTrace();
46                }
47                synchronized (o1) {
48                    System.out.println("线程2成功拿到两把锁");
49                }
50            }
51        }
52    }
53}

  • 查到上面执行的程序的进程pid,执行:D:\Program Files\Java\jdk1.8.0_172\bin>jstack.exe 108352
Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x000000001be53948 (object 0x0000000780caf9a0, a java.lang.Object),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x000000001a93cd18 (object 0x0000000780caf9b0, a java.lang.Object),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
        at ConcurrenceFolder.mooc.threadConcurrencyCore.deadlock.MustDeadLock.run(MustDeadLock.java:48)
        - waiting to lock <0x0000000780caf9a0> (a java.lang.Object)
        - locked <0x0000000780caf9b0> (a java.lang.Object)
        at java.lang.Thread.run(Thread.java:748)
"Thread-0":
        at ConcurrenceFolder.mooc.threadConcurrencyCore.deadlock.MustDeadLock.run(MustDeadLock.java:36)
        - waiting to lock <0x0000000780caf9b0> (a java.lang.Object)
        - locked <0x0000000780caf9a0> (a java.lang.Object)
        at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.
  • Thread1 lock f9b0,waiting f9a0
  • Thread0 lock f9a0,waiting f9b0
  • 同时也显示了死锁的位置MustDeadLock.java:48和MustDeadLock.java:36

4.2 ThreadMXBean代码检测

**
 * MustDeadLock
 *
 * @author venlenter
 * @Description: 用ThreadMXBean检测死锁
 * @since unknown, 2020-06-10
 */
public class ThreadMXBeanDetection implements Runnable {
    int flag = 1;
    static Object o1 = new Object();
    static Object o2 = new Object();

    public static void main(String[] args) throws InterruptedException {
        ThreadMXBeanDetection r1 = new ThreadMXBeanDetection();
        ThreadMXBeanDetection r2 = new ThreadMXBeanDetection();
        r1.flag = 1;
        r2.flag = 0;
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);
        t1.start();
        t2.start();
        Thread.sleep(1000);
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        long[] deadLockedThreads = threadMXBean.findDeadlockedThreads();
        if (deadLockedThreads != null && deadLockedThreads.length > 0) {
            for (int i = 0; i < deadLockedThreads.length; i++) {
                ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadLockedThreads[i]);
                System.out.println("发现死锁:" + threadInfo.getThreadName());
            }
        }
    }
    @Override
    public void run() {
        System.out.println("flag = " + flag);
        if (flag == 1) {
            synchronized (o1) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o2) {
                    System.out.println("线程1成功拿到两把锁");
                }
            }
        }
        if (flag == 0) {
            synchronized (o2) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o1) {
                    System.out.println("线程2成功拿到两把锁");
                }
            }
        }
    }
}

//输出结果
flag = 1
flag = 0
发现死锁:Thread-1
发现死锁:Thread-0

5、修复死锁的策略

5.1 线上发生死锁应该怎么办?

  • 线上问题都需要防患于未然,不造成损失地扑灭几乎已经是不可能
  • 保存案发现场然后立刻重启服务器
  • 暂时保证线上服务的安全,然后再利用刚才保存的信息,排查死锁,修改代码,重新发版

5.2 常见修复策略

  • 避免策略:【哲学家就餐】的换手方案、转账换序方案(思路:避免相反的获取锁的顺序)
  • 检测与恢复策略:一段时间检测是否有死锁,如果有就剥夺某个资源,来打开死锁

5.2.1 转账时避免死锁(转账换序方案)

  • 实际上不在乎获取锁的顺序
  • 代码演示
  • 通过【hashcode】来决定获取锁的顺序、冲突时需要“加时赛”
/**
 * TransferMoney
 *
 * @author venlenter
 * @Description: 转账时通过【hashcode】来决定获取锁的顺序,避免死锁
 * @since unknown, 2020-06-13
 */
public class TransferMoney implements Runnable {
    int flag = 1;
    static Account a = new Account(500);
    static Account b = new Account(500);
    static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        TransferMoney r1 = new TransferMoney();
        TransferMoney r2 = new TransferMoney();
        r1.flag = 1;
        r2.flag = 0;
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("a的余额" + a.balance);
        System.out.println("b的余额" + b.balance);
    }

    @Override
    public void run() {
        if (flag == 1) {
            transferMoney(a, b, 200);
        }
        if (flag == 0) {
            transferMoney(b, a, 200);
        }
    }

    public static void transferMoney(Account from, Account to, int amount) {
        //增加内部类
        class Helper {
            public void transfer() {
                if (from.balance - amount < 0) {
                    System.out.println("余额不足,转账失败");
                }
                from.balance -= amount;
                to.balance += amount;
                System.out.println("成功转账" + amount + "元");
            }
        }
        int fromHash = System.identityHashCode(from);
        int toHash = System.identityHashCode(to);
        //通过通过【hashcode】来决定获取锁的顺序
        if (fromHash < toHash) {
            synchronized (from) {
                synchronized (to) {
                    new Helper().transfer();
                }
            }
        } else if (fromHash > toHash) {
            synchronized (to) {
                synchronized (from) {
                    new Helper().transfer();
                }
            }
        } else {
            //当hashcode相同的时候,冲突时需要“加时赛”,用额外的lock锁
            synchronized (lock) {
                synchronized (to) {
                    synchronized (from) {
                        new Helper().transfer();
                    }
                }
            }
        }
    }

    static class Account {
        public Account(int balance) {
            this.balance = balance;
        }

        int balance;
    }
}
  • 如果实体有【主键】就更方便

5.2.2 哲学家就餐问题

(1)问题描述

  • 流程
①先拿起左手的筷子
②然后拿起右手的筷子
③如果筷子被人使用了,那就等别人用完
④吃完后,把筷子放回原位

(2)有【死锁】和【资源耗尽】的风险

  • 死锁:每个哲学家都拿着左手的筷子,【永远都在等右边】的筷子(或相反)

(3)代码演示:哲学家进入死锁

/**
 * DiningPhilosophers
 *
 * @author venlenter
 * @Description: 演示哲学家就餐问题导致的死锁
 * @since unknown, 2020-06-14
 */
public class DiningPhilosophers {
    public static class Philosopher implements Runnable {
        private Object leftChopstick;
        private Object rightChopstick;

        public Philosopher(Object leftChopstick, Object rightChopstick) {
            this.leftChopstick = leftChopstick;
            this.rightChopstick = rightChopstick;
        }

        @Override
        public void run() {
            try {
                while (true) {
                    doAction("Thinking");
                    synchronized (leftChopstick) {
                        doAction("Picked up left chopstick");
                        synchronized (rightChopstick) {
                            doAction("Pick up right chopstick - eating");
                            doAction("Put down right chopstick");
                        }
                        doAction("Put down left chopstick");
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        private void doAction(String action) throws InterruptedException {
            System.out.println(Thread.currentThread().getName() + " " + action);
            Thread.sleep((long) (Math.random() * 10));
        }
    }

    public static void main(String[] args) {
        Philosopher[] philosophers = new Philosopher[5];
        Object[] chopsticks = new Object[philosophers.length];
        for (int i = 0; i < chopsticks.length; i++) {
            chopsticks[i] = new Object();
        }
        for (int i = 0; i < philosophers.length; i++) {
            Object leftChopstick = chopsticks[i];
            Object rightChopstick = chopsticks[(i + 1) % philosophers.length];
            philosophers[i] = new Philosopher(leftChopstick, rightChopstick);
            new Thread(philosophers[i], "哲学家" + (i + 1) + "号").start();

        }
    }
}
//输出结果
哲学家1号 Thinking
哲学家2号 Thinking
哲学家3号 Thinking
哲学家4号 Thinking
哲学家5号 Thinking
哲学家2号 Picked up left chopstick
哲学家1号 Picked up left chopstick
哲学家5号 Picked up left chopstick
哲学家3号 Picked up left chopstick
哲学家4号 Picked up left chopstick
//程序卡住,死锁

(4)多种解决方案

  • 服务员检查(避免策略):服务员检查是否会陷入死锁,如果检查可能存在,则让你先停止请求吃饭
  • 【改变一个哲学家拿筷子的顺序(避免策略)】
  • 餐票(避免策略):事先提供允许吃饭的餐票,只有拿到餐票的才可以执行吃饭
  • 领导调节(检测与恢复策略);让程序正常执行,当发现死锁的时候,有一个外部指令进来中止其中一个线程,相当于破坏掉死锁的“不剥夺条件”(线程A持有lock-a,外界不能剥夺A对lock-a的持有)

(5)代码演示:解决死锁

  • 【改变一个哲学家拿筷子的顺序(避免策略)】
/**
 * DiningPhilosophers
 *
 * @author venlenter
 * @Description: 演示哲学家就餐问题导致的死锁
 * @since unknown, 2020-06-14
 */
public class DiningPhilosophers {
    public static class Philosopher implements Runnable {
        private Object leftChopstick;
        private Object rightChopstick;

        public Philosopher(Object leftChopstick, Object rightChopstick) {
            this.leftChopstick = leftChopstick;
            this.rightChopstick = rightChopstick;
        }

        @Override
        public void run() {
            try {
                while (true) {
                    doAction("Thinking");
                    synchronized (leftChopstick) {
                        doAction("Picked up left chopstick");
                        synchronized (rightChopstick) {
                            doAction("Pick up right chopstick - eating");
                            doAction("Put down right chopstick");
                        }
                        doAction("Put down left chopstick");
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        private void doAction(String action) throws InterruptedException {
            System.out.println(Thread.currentThread().getName() + " " + action);
            Thread.sleep((long) (Math.random() * 10));
        }
    }

    public static void main(String[] args) {
        Philosopher[] philosophers = new Philosopher[5];
        Object[] chopsticks = new Object[philosophers.length];
        for (int i = 0; i < chopsticks.length; i++) {
            chopsticks[i] = new Object();
        }
        for (int i = 0; i < philosophers.length; i++) {
            Object leftChopstick = chopsticks[i];
            Object rightChopstick = chopsticks[(i + 1) % philosophers.length];
            //改进:当是最后一个哲学家,则反过来,先取右边的筷子
            if (i == philosophers.length - 1) {
                philosophers[i] = new Philosopher(rightChopstick, leftChopstick);
            } else {
                philosophers[i] = new Philosopher(leftChopstick, rightChopstick);
            }
            new Thread(philosophers[i], "哲学家" + (i + 1) + "号").start();
        }
    }
}
//输出结果
//长时间打印,没有处于死锁状态

5.2.3 死锁检测与恢复策略

(1)检测算法:锁的调用链路图

  • 允许发生死锁
  • 每次调用锁都记录
  • 定期检查“锁的调用链路图”中是否存在环路
  • 一旦发生死锁,就用死锁恢复机制进行恢复

(2)恢复方法1:【进程中止】

  • 【逐个终止】线程,直到死锁消除
  • 终止顺序
①优先级(是前台交互还是后台处理)
②已占用资源、还需要的资源(还需要一点资源就可以完成任务的,则优先执行,终止其他的)
③已运行时间(已运行较长时间,快要完成任务的,则优先执行,终止其他的)

(3)恢复方法2:资源抢占

  • 把已经分发出去的锁给【收回来】
  • 让线程【回退几步】,这样就不用结束整个线程,【成本比较低】
  • 缺点:可能同一个线程一直被抢占,那就造成【饥饿】

6、实际工程中如何避免死锁

6.1 设置【超时】时间

  • Lock的tryLock(long timeout, TimeUnit unit)
  • synchronized不具备尝试锁的能力
  • 造成超时的可能性多:发生了死锁、线程陷入死循环、线程执行很慢
  • 获取锁失败时:打印错误日志、发报警邮件、重启等
/**
 * TryLockDeadlock
 *
 * @author venlenter
 * @Description: 用tryLock来避免死锁
 * @since unknown, 2020-06-14
 */
public class TryLockDeadlock implements Runnable {
    int flag = 1;
    static Lock lock1 = new ReentrantLock();
    static Lock lock2 = new ReentrantLock();

    public static void main(String[] args) {
        TryLockDeadlock r1 = new TryLockDeadlock();
        TryLockDeadlock r2 = new TryLockDeadlock();
        r1.flag = 1;
        r2.flag = 0;
        new Thread(r1).start();
        new Thread(r2).start();
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (flag ==1) {
                try {
                    if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
                        System.out.println("线程1获取到了锁1");
                        Thread.sleep(new Random().nextInt(1000));
                        if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {
                            System.out.println("线程1获取到了锁2");
                            System.out.println("线程1成功获取到了两把锁,释放全部锁");
                            lock2.unlock();
                            lock1.unlock();
                            break;
                        } else {
                            System.out.println("线程1尝试获取锁2失败,已重试,释放锁1");
                            lock1.unlock();
                            Thread.sleep(new Random().nextInt(1000));
                        }
                    } else {
                        System.out.println("线程1获取锁1失败,已重试");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            if (flag ==0) {
                try {
                    if (lock2.tryLock(3000, TimeUnit.MILLISECONDS)) {
                        System.out.println("线程2获取到了锁2");
                        Thread.sleep(new Random().nextInt(1000));
                        if (lock1.tryLock(3000, TimeUnit.MILLISECONDS)) {
                            System.out.println("线程2获取到了锁1");
                            System.out.println("线程2成功获取到了两把锁,释放全部锁");
                            lock1.unlock();
                            lock2.unlock();
                            break;
                        } else {
                            System.out.println("线程2尝试获取锁1失败,已重试,释放锁2");
                            lock2.unlock();
                            Thread.sleep(new Random().nextInt(1000));
                        }
                    } else {
                        System.out.println("线程2获取锁2失败,已重试");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
//输出结果
线程1获取到了锁1
线程2获取到了锁2
线程1尝试获取锁2失败,已重试,释放锁1
线程2获取到了锁1
线程2成功获取到了两把锁,释放全部锁
线程1获取到了锁1
线程1获取到了锁2
线程1成功获取到了两把锁,释放全部锁

6.2 多使用【并发类】而不是自己设计锁

  • ConcurrentHashMap、ConcurrentLinkedQueue、AtomicBoolean等
  • 实际应用中java.util.concurrent.atomic十分有用,简单方便且效率比使用Lock更高
  • 多用【并发集合】少用同步集合(Collections.synchronizedMap()和Collections.synchronizedList()),并发集合比同步集合的可扩展性更好
  • 并发场景需要用到map,首先想到用【ConcurrentHashMap】

6.3 尽量降低锁的使用【粒度】:用不同的锁而不是一个锁

6.4 如果能使用【同步代码块】,就不使用同步方法:方便自己指定锁对象,而不是直接整个方法

6.5 给线程起一个有意义的名字:debug和排查时事半功倍,框架和JDK都遵循这个最佳实践

6.6 避免锁的【嵌套】:MustDeadLock类

synchronized(lock1) {
    synchronized(lock2) {
	    //xxx
	}
}

6.7 分配资源前先看下能不能收回来:银行家算法

6.8 尽量不要几个功能用同一把锁:【专锁专用】

7、其他活性故障

  • 死锁是最常见的活跃性问题,不过除了刚才的死锁之外,还有一些类似的问题,会导致程序无法顺利执行,统称为活跃性问题
  • 【活锁(LiveLock)】
  • 【饥饿】

7.1 活锁

7.1.1 什么是活锁

  • 虽然线程并没有阻塞,也【始终在运行】(所以叫做“活”锁,线程是“活”的),但程序却【得不到进展】,因为线程始终重复做同样的事(一直询问请求对方的锁)(同时占用着CPU)
  • 如果是死锁,那么就是阻塞,相互等待(不占用CPU)
  • 死锁和活锁的【结果是一样的】,就是相互等待着

7.1.2 代码演示

/**
 * LiveLock
 *
 * @author venlenter
 * @Description: 演示活锁问题
 * @since unknown, 2020-06-15
 */
public class LiveLock {
    static class Spoon {
        private Diner owner;

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

        public Diner getOwner() {
            return owner;
        }

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

        public synchronized void use() {
            System.out.printf("%s吃完了!", owner.name);
        }
    }

    static class Diner {
        private String name;
        private boolean isHunger;

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

        public void eatWith(Spoon spoon, Diner spouse) {
            while (isHunger) {
                if (spoon.owner != this) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    continue;
                }
                if (spouse.isHunger) {
                    System.out.println(name + " : 亲爱的" + spouse.name + "你先吃吧");
                    spoon.setOwner(spouse);
                    continue;
                }
                spoon.use();
                isHunger = false;
                System.out.println(name + " : " + "我吃完了");
                spoon.setOwner(spouse);
            }
        }
    }

    public static void main(String[] args) {
        Diner husband = new Diner("牛郎");
        Diner wife = new Diner("织女");
        Spoon spoon = new Spoon(husband);
        new Thread(() -> husband.eatWith(spoon, wife)).start();
        new Thread(() -> wife.eatWith(spoon, husband)).start();
    }
}
//输出结果
牛郎 : 亲爱的织女你先吃吧
织女 : 亲爱的牛郎你先吃吧
牛郎 : 亲爱的织女你先吃吧
织女 : 亲爱的牛郎你先吃吧
...//一直循环交替输出,不停止
牛郎 : 亲爱的织女你先吃吧
织女 : 亲爱的牛郎你先吃吧
牛郎 : 亲爱的织女你先吃吧
织女 : 亲爱的牛郎你先吃吧

7.1.3 如何解决活锁问题

  • 原因:重试机制不变,消息队列始终重试,【吃饭始终谦让】
  • 以太网的指数【退避】算法:双方以随机时间等待后再重试,不会因为再次同时碰撞
  • 加入【随机】因素
/**
 * LiveLock
 *
 * @author venlenter
 * @Description: 演示活锁问题
 * @since unknown, 2020-06-15
 */
public class LiveLock {
    static class Spoon {
        private Diner owner;

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

        public Diner getOwner() {
            return owner;
        }

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

        public synchronized void use() {
            System.out.printf("%s吃完了!", owner.name);
        }
    }

    static class Diner {
        private String name;
        private boolean isHunger;

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

        public void eatWith(Spoon spoon, Diner spouse) {
            while (isHunger) {
                if (spoon.owner != this) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    continue;
                }
                Random random = new Random();
                //加入随机因素
                if (spouse.isHunger && random.nextInt(10) < 9) {
                    System.out.println(name + " : 亲爱的" + spouse.name + "你先吃吧");
                    spoon.setOwner(spouse);
                    continue;
                }
                spoon.use();
                isHunger = false;
                System.out.println(name + " : " + "我吃完了");
                spoon.setOwner(spouse);
            }
        }
    }

    public static void main(String[] args) {
        Diner husband = new Diner("牛郎");
        Diner wife = new Diner("织女");
        Spoon spoon = new Spoon(husband);
        new Thread(() -> husband.eatWith(spoon, wife)).start();
        new Thread(() -> wife.eatWith(spoon, husband)).start();
    }
}
//输出结果
牛郎 : 亲爱的织女你先吃吧
织女 : 亲爱的牛郎你先吃吧
牛郎 : 亲爱的织女你先吃吧
织女吃完了!织女 : 我吃完了
牛郎吃完了!牛郎 : 我吃完了

7.1.4 工程中的活锁实例:【消息队列】

  • 错误方法:消息处理失败时,如果放到队列开头重试,当服务出了问题,处理该消息一直失败,则会导致程序一直卡着
  • 解决:【将失败的消息放到队列尾部】、重试限制(比如限制重连3次,超过3次就做其他的逻辑)

7.2 饥饿

  • 当线程需要某些资源(例如CPU),但却【始终得不到】
  • 线程的【优先级】设置得过于低(如设置为1),或者有线程持有锁同时又无限循环从而【不释放锁】,或者某程序【始终占用】某文件的【写锁】
  • 饥饿可能会导致【响应性差】:比如,浏览器有A线程负责前台响应(打开收藏夹等动作),B线程负责后台下载图片和文件、计算渲染等。如果后台线程B把CPU资源都占用了,那么前台线程A将无法得到很好地执行,这会导致用户体验很差

8、常见面试问题

(1)写一个【必然死锁】的例子,生产中什么场景下会发生死锁?

  • 例子:线程设置flag区分启动,相互调用对方的锁(AB,BA)
  • 什么场景下会发生死锁:相互调用锁
/**
 * MustDeadLock
 *
 * @author venlenter
 * @Description: 必定发生死锁的情况
 * @since unknown, 2020-06-10
 */
public class MustDeadLock implements Runnable {
    int flag = 1;
    static Object o1 = new Object();
    static Object o2 = new Object();

    public static void main(String[] args) {
        MustDeadLock r1 = new MustDeadLock();
        MustDeadLock r2 = new MustDeadLock();
        r1.flag = 1;
        r2.flag = 0;
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);
        t1.start();
        t2.start();
    }
    @Override
    public void run() {
        System.out.println("flag = " + flag);
        if (flag == 1) {
            synchronized (o1) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o2) {
                    System.out.println("线程1成功拿到两把锁");
                }
            }
        }
        if (flag == 0) {
            synchronized (o2) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (o1) {
                    System.out.println("线程2成功拿到两把锁");
                }
            }
        }
    }
}

2、发生死锁必须满足【哪些条件】?

  • 互斥条件(线程A拿到了锁lock-a,则其他线程要获取lock-a时则只能等待)
  • 请求与保持条件(线程A在请求lock-b的时候,同时保持着lock-a锁)
  • 不剥夺条件(线程A持有lock-a,外界不能剥夺A对lock-a的持有)
  • 循环等待条件(多个线程形成环路,A等待B,B等待C,C等待A)

3、如何【定位】死锁

  • jstack:发生死锁后,通过pid dump出线程详情
  • ThreadMXBean:代码中检测

4、有哪些【解决】死锁问题的【策略】?

  • 避免策略:【哲学家就餐】的换手方案(最后一个人切换方向)、转账换序方案(通过【hashcode】来决定获取锁的顺序)
  • 检测与恢复策略:一段时间【检测】是否有死锁,如果有就【剥夺】某个资源,来打开死锁
  • 鸵鸟策略:不推荐

5、讲一讲经典的【哲学家就餐】问题

  • 解决方案

6、实际工程中如何【避免死锁】?

  • ①设置【超时】时间
  • ②多使用【并发类】而不是自己设计锁
  • ③尽量降低锁的使用【粒度】:用不同的锁而不是一个锁
  • ④如果能使用【同步代码块】,就不使用同步方法:方便自己指定锁对象,而不是直接整个方法
  • ⑤给线程起一个有意义的名字:debug和排查时事半功倍,框架和JDK都遵循这个最佳实践
  • ⑥避免锁的【嵌套】:MustDeadLock类
  • ⑦分配资源钱先看下能不能收回来:银行家算法
  • ⑧尽量不要几个功能用同一把锁:【专锁专用】

7、什么是活跃性问题?活锁、饥饿和死锁有什么区别?

 

笔记来源:慕课网悟空老师视频《Java并发核心知识体系精讲》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章