活跃性问题(死锁)

什么是死锁?

  1. 发生在并发里。

  2. 互不相让:当两个(或多个)线程(或进程)相互持有对方所需要的锁,又不主动释放,导致所有人都无法继续前进,导致程序陷入无尽的阻塞,这就是死锁。
    在这里插入图片描述

  3. 如果多个线程之间的依赖关系是环形,存在环路的锁的依赖关系,那么也可能发生死锁。
    在这里插入图片描述

  • 死锁的影响

  • 几率不高单危害大

必定发生死锁最简单的例子

/**
 * 必定发生死锁
 */
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成功拿到两把锁");
                }
            }
        }
    }
}

转账时候遇到死锁,一旦打开注释,就是发生死锁 例子

/**
 * 转账时候遇到死锁,一旦打开注释,就是发生死锁
 */
public class TransferMoney implements Runnable {
    private 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;
    }
}

多人转账死锁例子

/**
 * 多人同时转账,依然很危险(演示多人转账死锁)
 */
public class MultiTransferMoney {

    private static final int NUM_ACCOUNRS = 5000;
    private static final int NUM_MONEY = 1000;
    private static final int NUM_ITERATIONS = 1000000;
    private static final int NUM_THREADS = 20;

    public static void main(String[] args) {
        Random rnd = new Random();
        TransferMoney.Account[] accounts = new TransferMoney.Account[NUM_ACCOUNRS];
        for(int i =0; i<accounts.length;i++ ){
            accounts[i] = new TransferMoney.Account(NUM_MONEY);
        }
        class TransferThread extends Thread{
            @Override
            public void run(){
                for(int i = 0;i< NUM_ITERATIONS;i++){
                    int fromAcct = rnd.nextInt(NUM_ACCOUNRS);
                    int toAcct = rnd.nextInt(NUM_ACCOUNRS);
                    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();
        }
    }
}

* 死锁发生的4个必要条件(缺一不可)

  1. 互斥条件
  2. 请求与保持条件
  3. 不剥夺条件
  4. 循环等待条件
    判断程序死锁就对着这四点,都满足,就会发生死锁

检测定位死锁

  • jstake
  • ThreadMXBean
    代码演示
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;

/**
 * 用ThreadMXBean检测死锁
 */
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 {
        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();
        Thread.sleep(1000);

        //用ThreadMXBean检测死锁
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        //拿到所有死锁线程id
        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

修复策略

* 避免策略

  • 转账转换方案
    1. 实际上不在乎获取锁的顺序
    2. 通过hashCode来决定获取锁的顺序,冲突时需要“加时赛”
    3. 有主键更方便
/**
 * 修复转账死锁问题
 */
public class TransferMoney implements Runnable {
    private 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 + "元");
            }
        }
        //获取两个对象的hashCode
        int fromHash = System.identityHashCode(from);
        int toHash = System.identityHashCode(to);
        //根据hash值大小不同加锁顺序调换
        if (fromHash < toHash) {
            synchronized (from) {
                synchronized (to) {
                    new Helper().transfer();
                }
            }
        } else if (fromHash > toHash) {
            synchronized (to) {
                synchronized (from) {
                    new Helper().transfer();
                }
            }
        } else {//当hash值一样
            synchronized (lock){
                synchronized (to) {
                    synchronized (from) {
                        new Helper().transfer();
                    }
                }
            }
        }

    }

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

哲学家就餐问题

  1. 问题描述
    在这里插入图片描述
    想吃饭,先拿起左手边的筷子,然后拿起右手边的筷子,如果筷子被人用了,那就等别人用完,吃完后加个筷子放回原位。

  2. 有死锁和耗尽资源风险

  3. 代码演示:哲学家进入死锁
    死锁:每个哲学家都拿起左手边的筷子,永远都在等右边的筷子(或者相反)

/**
 * 演示哲学家就餐导致死锁
 */
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("Picked up right chopstick -eating");

                            doAction("Picked down right chopstick");
                        }
                        doAction("Picked 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) % chopsticks.length];
            philosophers[i] = new Philosopher(leftChopstick, rightChopstick);
            new Thread(philosophers[i], "哲学家" + (i + 1) + "号").start();
        }
    }
}

众多结果里的其中一种结果:

哲学家1号 Thinking
哲学家3号 Thinking
哲学家4号 Thinking
哲学家2号 Thinking
哲学家5号 Thinking
哲学家2号 Picked up left chopstick
哲学家4号 Picked up left chopstick
哲学家1号 Picked up left chopstick
哲学家3号 Picked up left chopstick
哲学家5号 Picked up left chopstick
  1. 多种解决方案
    a. 服务员检查(避免策略),相当于引入了外界的协调
    b. 改变一个哲学家拿筷子的顺序(避免策略)
    c.餐票(避免策略)每个人只能拿到餐票才能吃,5个人吃饭,我们只提供4个餐票
    d.领导调节(检测与恢复策略)相当于当死锁有有一个机制能让某个人释放资源
  2. 代码演示:解决死锁
/**
* 避免策略:哲学家就餐的换手方案
* b. 改变一个哲学家拿筷子的顺序(避免策略)
 * 只贴出主方法,因为其他代码合上面的一致
 * 主要看这个if里的初始化哲学家的代码 if(i == philosophers.length - 1){
 */
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) % chopsticks.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();
        }
    }

常见修复策略

  • 避免策略:****哲学家就餐的换手方案。转账换序方案
  • 检测与恢复策略:一段时间检测是否有死锁,如果有就剥夺其中一个资源,来打开死锁
  • 鸵鸟策略:如果我们发生死锁的几率非常低,处理成本大,就可以直接忽略它,直到死锁发生后再人工修复

实际工程中如何避免死锁

  1. 设置超时时间

    • Lock的tryLock(long timeout,TimeUnit unit)(后面有代码演示)。
    • synchronized不具备尝试锁的能力。
    • 造成超时的可能性很多:发生了死锁、线程进入了死循环、线程本身就执行很慢。
    • 获取锁失败:打日志、发警报邮件、重启
  2. 多使用并发类而不是自己设计锁

    • ConcurrentHashMap、ConcurrentLinkedQueue、AtomicBoolean等。
    • 实际应用中java.util.concurrent.atomic是非常有用的,简单方便且效率比使用Lock更高。
    • 多用并发集合少用同步集合,并发集合比同步集合的可扩展性更好 。
    • 并发场景需要用到Map,首先想到用ConcurrentHashMap
  3. 降低锁的力度:用不同的锁,而不是一个锁。

  4. 如果能使用同步代码块,就不使用同步方法:自己指定锁的对象。

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

  6. 避免锁的嵌套:如上面的MustDeadLock 类。

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

  8. 尽量不要几个功能使用同一把锁:专锁专用

Lock的tryLock避免死锁的代码演示

/**
 * 用tryLock避免死锁
 */
public class TryLockDeadLock implements Runnable{
    static Lock lock1 = new ReentrantLock();
    static Lock lock2 = new ReentrantLock();
    private int flag = 1;

    public static void main(String[] args) {
        TryLockDeadLock r1 = new TryLockDeadLock();
        TryLockDeadLock r2 = new TryLockDeadLock();
        r1.flag = 1;
        r1.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");
                        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失败,已重试");
                            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");
                        if(lock1.tryLock(3000,TimeUnit.MILLISECONDS)){
                            System.out.println("线程2获取到了锁1");
                            System.out.println("线程2获取到了两把锁");
                            lock2.unlock();
                            lock1.unlock();
                            break;
                        }else {
                            System.out.println("线程2尝试锁1失败,已重试");
                            lock1.unlock();
                            Thread.sleep(new Random().nextInt(1000));
                        }
                    }else{
                        System.out.println("线程2获取锁2失败,已重试");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

上篇:单例模式的8种写法
下篇:活跃性问题(活锁、饥饿)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值