Java并发编程(四)

死锁

  • 发生在并发中
  • 互不相让:当两个或更多的线程/进程 相互持有对方所需要的资源,又不主动释放,导致所有人都无法继续前进,导致程序陷入无尽的阻塞,这就是死锁
    在这里插入图片描述
  • 多个线程造成死锁的情况:如果多个线程之间的依赖关系是环形,存在环路的锁的依赖关系,那么也可能发生死锁
    在这里插入图片描述

死锁的影响

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

  • 数据库中:检测并放弃事务
  • JVM中:无法自动处理(为了安全考虑)

发生死锁的几率不高,但危害很大。多发生在高并发场景,影响用户多。使整个系统崩溃、子系统崩溃、性能降低。

死锁例子:

/**
 * 描述:     必定发生死锁的情况
 */
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


public class DieLock {
 
    public static Object t1 = new Object();
    public static Object t2 = new Object();
 
    public static void main(String[] args){
        new Thread(){
            @Override
            public void run(){
                synchronized (t1){
                    System.out.println("Thread1 get t1");
 
                    try {
                        Thread.sleep(100);
                    }catch (Exception e){
 
                    }
 
                    synchronized (t2){
                        System.out.println("Thread2 get t2");
                    }
                }
            }
        }.start();
 
        new Thread(){
            @Override
            public void run(){
                synchronized (t2){
                    System.out.println("Thread2 get t2");
 
                    try {
                        Thread.sleep(100);
                    }catch (Exception e){
 
                    }
 
                    synchronized (t1){
                        System.out.println("Thread2 get t1");
                    }
                }
            }
        }.start();
    }
}

实际成产中的例子:转账

package deadlock;

/**
 * 描述:     转账时候遇到死锁,一旦打开注释,便会发生死锁
 */
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("余额不足,转账失败。");
                    return;
                }
                from.balance -= amount;
                to.balance = to.balance + amount;
                System.out.println("成功转账" + amount + "元");
            }
        }
        int fromHash = System.identityHashCode(from);
        int toHash = System.identityHashCode(to);
        if (fromHash < toHash) {
            synchronized (from) {
                synchronized (to) {
                    new Helper().transfer();
                }
            }
        }
        else if (fromHash > toHash) {
            synchronized (to) {
                synchronized (from) {
                    new Helper().transfer();
                }
            }
        }else  {
            synchronized (lock) {
                synchronized (to) {
                    synchronized (from) {
                        new Helper().transfer();
                    }
                }
            }
        }

    }


    static class Account {

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

        int balance;

    }
}

死锁的必要条件

以下条件缺一不可

  • 互斥条件:一个锁同一时刻只能被一个线程所获取
  • 请求与保持条件:既想请求获取新的锁,又不愿意释放原有的锁
  • 不剥夺条件:当想获取某把锁,而在等待某线程释放该锁时,不能去剥夺此时拥有该锁的线程的持有权
  • 循环等待条件:一直等待直到想获取的锁被其他线程释放

死锁的定位

1.jstack [pid]

  • 用 Sloth 或者命令行查看到 java 的pid
  • 执行 ${JAVA_HOME}/bin/jstack pid(不是百分百能起作用)
    在这里插入图片描述
    2.使用 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 {
        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成功拿到两把锁");
                }
            }
        }
    }
}

死锁的修复策略

常见修策略

  • 避免策略:哲学家就餐的换手方案、转账换序方案

  • 检测与恢复策略:一段时间检测是否有死锁,如果有就剥夺某一个资源,来打开死锁

  • 鸵鸟策略:如果我们发生死锁的概率极其低,那么我们就直接忽略他,直到死锁发生时,再人工修复(no)

哲学家就餐问题(末尾有问题的详细内容)的多种解决方案
联系死锁的四个必要条件

i)服务员检查(避免策略)
引入一个餐厅服务生,哲学家必须经过他的允许才能拿起餐叉。因为服务生知道哪只餐叉正在使用,所以他能够作出判断避免死锁。

ii)改变一个哲学家拿叉子的顺序(避免策略)
不会造成互相等待,以为改变顺序后该哲学家可以同时拿到两根叉子

iii)餐票(避免策略)

iiii)领导调节(检测与恢复策略)
即剥夺某个哲学家的叉子使用权,让给其他人

检测算法:锁的调用链路图
避免策略:进程终止

  • 逐个终止进程,直到死锁消除
  • 终止顺序
    i)优先级(前台交互还是后台处理)
    ii)已占用资源,还需要资源
    iii)已经运行时间

检测恢复策略:资源抢占

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

实际工程中避免死锁的措施

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.避免锁的嵌套:前面的 MustDaadLock 示例

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

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

其他活性故障

1.死锁是最常见的活跃性问题,不过除了前面的死锁之外,还有一些类似的问题,会导致程序无法顺利执行,统称为活跃性问题

2.活锁(LiveLock)
i)虽然线程并没有阻塞,也始终在运行(线程是活的),但是程序却得不到进展,因为线程始终重复做同样的事情——重复工作
ii)相比之下,死锁是直接两个线程都始终一动不动,直到一方先释放,线程间并不做其他事情,只是一直等待

示例:


/**
 * 描述:     演示活锁问题
 */
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 isHungry;

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

        public void eatWith(Spoon spoon, Diner spouse) {
            while (isHungry) {
                if (spoon.owner != this) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    continue;
                }
                Random random = new Random();
                if (spouse.isHungry && random.nextInt(10) < 9) {
                    System.out.println(name + ": 亲爱的" + spouse.name + "你先吃吧");
                    spoon.setOwner(spouse);
                    continue;
                }

                spoon.use();
                isHungry = 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(new Runnable() {
            @Override
            public void run() {
                husband.eatWith(spoon, wife);
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                wife.eatWith(spoon, husband);
            }
        }).start();
    }
}

影响:有死锁和资源耗尽的风险
解决方法

  • 以太网的指数退避算法
  • 加入随机因素

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

  • 策略:消息如果处理失败,就放在队列开头重试
  • 由于依赖服务出了问题,处理该消息一直失败
  • 没阻塞,当程序无法继续
  • 解决:放到队列尾部、重试限制

3.饥饿

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

程序设计不应该依赖于优先级

  • 不同操作系统不一样
  • 优先级会被操作系统改变

哲学家就餐问题内容:

哲学家就餐问题可以这样表述,假设有五位哲学家围坐在一张圆形餐桌旁,做以下两件事情之一:吃饭,或者思考。吃东西的时候,他们就停止思考,思考的时候也停止吃东西。餐桌中间有一大碗意大利面,每两个哲学家之间有一只餐叉。因为用一只餐叉很难吃到意大利面,所以假设哲学家必须用两只餐叉吃东西。他们只能使用自己左右手边的那两只餐叉。哲学家就餐问题有时也用米饭和筷子而不是意大利面和餐叉来描述,因为很明显,吃米饭必须用两根筷子。
哲学家从来不交谈,这就很危险,可能产生死锁,每个哲学家都拿着左手的餐叉,永远都在等右边的餐叉(或者相反)。即使没有死锁,也有可能发生资源耗尽。例如,假设规定当哲学家等待另一只餐叉超过五分钟后就放下自己手里的那一只餐叉,并且再等五分钟后进行下一次尝试。这个策略消除了死锁(系统总会进入到下一个状态),但仍然有可能发生“活锁”。如果五位哲学家在完全相同的时刻进入餐厅,并同时拿起左边的餐叉,那么这些哲学家就会等待五分钟,同时放下手中的餐叉,再等五分钟,又同时拿起这些餐叉。
在实际的计算机问题中,缺乏餐叉可以类比为缺乏共享资源。一种常用的计算机技术是资源加锁,用来保证在某个时刻,资源只能被一个程序或一段代码访问。当一个程序想要使用的资源已经被另一个程序锁定,它就等待资源解锁。当多个程序涉及到加锁的资源时,在某些情况下就有可能发生死锁。例如,某个程序需要访问两个文件,当两个这样的程序各锁了一个文件,那它们都在等待对方解锁另一个文件,而这永远不会发生

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值