死锁,动态死锁,活锁

首先说明一下,这篇博客需要:

1.需要有一定锁的基础知识,并且了解简单的锁机制,和死锁的产生原因。

2.需要一定的并发知识,及多线程知识。

3.以Java为载体

 

好吧,我们废话不多说,这就进入我们今天的主题。

 

 

熟悉并发编程的朋友应该对"死锁"(DeadLock)这个概念不会陌生,因为在我们的程序设计多个线程,多个锁,多个“竞态”资源的时候,如果处理不得当,就非常容易出现死锁。

 

问题1,死锁不能解决码?

 

熟悉JVM的朋友都知道,jdk1.2以前(绿色计划)我们的JVM采用的是用户态的线程,也就是我们Java的线程不需要操作系统的内核态线程的帮助,操作系统不会感知到用户态线程的存在。

死锁,动态死锁,活锁_第1张图片

 

但是在jdk1.2以后,JVM便不再采用用户态线程了,转而采用内核态线程(Kernel Thread),每一个处理器(CPU)对应多个内核态线程,每一个内核态线程对应多个轻量级进程,而我们Java中的线程就是映射到轻量级进程上的,所以,实际上就是将线程的创建,阻塞,调度等操作全部交由操作系统来完成。

死锁,动态死锁,活锁_第2张图片

 不是说锁呢嘛?扯社么Java线程实现?

我们通过Java的线程实现可以发现其实底层是交给操作系统实现的,所以操作系统对死锁的处理与解决方式也就是Java对死锁的处理方式。

首先要声明一点。死锁问题是可以在操作系统层面被消灭的,比如,“银行家算法”,但是成本实在是太高了,所以我们目前的操作系统选择“鸵鸟策略”,遇到死锁不管,等着重启。

一旦死锁发生则采取专门的措施,解除死锁并以最小的代价恢复操作系统运行。死锁的解除(关键是代价最小):
1)重新启动

2)撤消进程

3)剥夺资源

4)进程回退

所以我么得出结论,从实际触发,到目前为指,死锁问题是不可避免的。


 

我先把类的定义全部放在这里:

银行账户类Account:

/**
 * 账户类
 */
public class Account {

    //账户Id
    private Long id;
    //账户名称
    private String name;
    //账户余额
    private long money;
    //可重入锁
    private Lock lock;

    public Account() {
    }
    
    public Account(Long id, String name, long money) {
        this.id = id;
        this.name = name;
        this.money = money;
        this.lock = new ReentrantLock();
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public long getMoney() {
        return money;
    }

    public void setMoney(long money) {
        this.money = money;
    }
    public Lock getLock() {
        return lock;
    }

}

 

转账接口类transferAccount:

/**
 * 定义转账接口供银行使用
 */
public interface transferAccount {

    void Dotransfer(Account from,Account to,int moeny);

}

 

 

 

场景一:死锁的出现

 

public class BackOfDp extends Thread{

    private Account zs = new Account(123456L,"zs",2000L);
    private Account ls = new Account(654321L,"ls",2000L);

    @Test
    public void fun1() {


        //创建给张三给李四转账的线程
        TransferLs t1 = new TransferLs();
        //创建李四给张三转账的线程
        TransferZs t2 = new TransferZs();

        t1.start();
        t2.start();

        try{
            t1.join();
            t2.join();
        }catch (InterruptedException e){
            e.printStackTrace();
        }

        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("-------------------------------------------");

        System.out.println(zs.getMoney());
        System.out.println(ls.getMoney());



    }

    /**
     * 锁定逻辑是先锁支出对象,再锁存入对像
     *
     */

    //张三给李四转账的线程
    private class TransferLs extends Thread{
        @Override
        public void run() {
            synchronized (zs){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("我拿到了zs的锁");
                synchronized (ls){
                    System.out.println("我拿到了ls的锁");
                    zs.setMoney(zs.getMoney()-200);
                    ls.setMoney(ls.getMoney()+200);
                    System.out.println("从"+zs.getName()+"到"+ls.getName()+"的转账动作完成");
                }
            }
        }
    }
    //李四给张三转账的线程
    private class TransferZs extends Thread{
        @Override
        public void run() {
            synchronized (ls){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("我拿到了ls的锁");
                synchronized (zs){
                    System.out.println("我拿到了zs的锁");
                    ls.setMoney(ls.getMoney()-200);
                    zs.setMoney(zs.getMoney()+200);
                    System.out.println("从"+ls.getName()+"到"+zs.getName()+"的转账动作完成");
                }
            }
        }
    }

}

 

运行结果:出现死锁。


死锁,动态死锁,活锁_第3张图片

 

分析原因:

从程序上看,是两个线程互相持有了对方需要获取的锁,造成方都无法获取请求的锁,造成死锁。解决思路,改变锁的获取顺序,让两个线程的获取锁的顺序一致(就是要么都一起获取zs的锁,要么都一起获取ls的锁,不能一个获取zs的锁一个获取ls的锁,这样容易导致死锁)。既然有了思路,我们就来验证一下。

 

只需要将ls给zs转账的线程的锁定顺序修改为与zs给ls转账的顺序一致就可以了。

    //李四给张三转账的线程
    private class TransferZs extends Thread{
        @Override
        public void run() {
            synchronized (zs){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("我拿到了zs的锁");
                synchronized (ls){
                    System.out.println("我拿到了ls的锁");
                    zs.setMoney(zs.getMoney()-200);
                    ls.setMoney(ls.getMoney()+200);
                    System.out.println("从"+zs.getName()+"到"+ls.getName()+"的转账动作完成");
                }
            }
        }
    }

 

运行结果:死锁解决!

死锁,动态死锁,活锁_第4张图片

 

 

 

场景二:动态死锁的出现。

 

真的只要确保锁定的顺序一致就能保证不出现死锁嘛?我们还是才有上面的例子来看一看什么是动态死锁。

 

转账线程类Transfer_1:

/**
 * 版本1的转账操作可能会产生死锁。
 */
public class Transfer_1 extends Thread implements transferAccount{


    private Account from;

    private Account to;

    private int money;

    public Transfer_1(Account from,Account to,int money){
        this.from = from;
        this.to = to;
        this.money = money;
    }

    /**
     * 我们计划通过加锁来实现线程安全,采用的策略是先锁定支出账户,再锁定存入账户
     *
     * @param from
     * @param to
     * @param money
     *
     * 运行结果显示,发生了死锁:
     * 原因分析,我么虽然是根据先锁定支出账户,再锁定存入账户,但是,我们的支出,存入
     * 是通过参数传递过来的,所以,虽然看起来是有先后顺序的,但是因为参数是动态的,所
     * 以仍然无法保证锁定顺序,这就是动态死锁。所谓动态在我看来就是指传入参数是动态可
     * 变得,顺序无法根据根据简单得参数名称判断。
     */
    @Override
    public void Dotransfer(Account from, Account to, int money) {
        System.out.println("转账动作开始!");

        //锁定支出账户
        synchronized (from){
            System.out.println("我拿到了from的锁:"+from.getName());
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (to){
                System.out.println("我拿到了to的锁:"+to.getName());
                from.setMoney(from.getMoney()-money);
                to.setMoney(to.getMoney()+money);
                System.out.println("从"+from.getName()+"到"+to.getName()+"的转账动作完成");
            }
        }
    }

    @Override
    public void run() {
        Dotransfer(from,to,money);
    }
}

可以看出,我么是先锁定from,再锁定to,可以说是按顺序锁定了,但是因为from和to只是参数,是可变的,所以在传入参数不同的情况下,我们的锁定顺序依然是动态可变的。这就是动态死锁的出现场景。

 

 

 

测试类:

public class BackOfDp extends Thread{

    @Test
    public void fun1() {
        Account zs = new Account(123456L,"zs",2000L);
        Account ls = new Account(654321L,"ls",2000L);

        //创建给张三给李四转账的线程
        Transfer_1 t1 = new Transfer_1(zs,ls,200);
        //创建李四给张三转账的线程
        Transfer_1 t2 = new Transfer_1(ls,zs,300);

        t1.start();
        t2.start();

        try{
            t1.join();
            t2.join();
        }catch (InterruptedException e){
            e.printStackTrace();
        }

        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("-------------------------------------------");

        System.out.println(zs.getMoney());
        System.out.println(ls.getMoney());
    }
}

 

运行结果:产生动态死锁

死锁,动态死锁,活锁_第5张图片

 

 

 

 

 

场景三,根据传入对象的hashCode硬性确定加锁顺序,消除可变性,避免死锁。

 

转账线程类Transfer_2:

/**
 * 解决动态死锁问题。
 */
public class Transfer_2 extends Thread implements transferAccount{


    private Account from;

    private Account to;

    private int money;

    public Transfer_2(Account from,Account to,int money){
        this.from = from;
        this.to = to;
        this.money = money;
    }

    //这个对象是用来当作一个锁的监视器的。解决Hash碰撞问题
    private Object locktie = new Object();
    /**
     * 解决思路:
     * 我们经过上一次得失败,明白了不能依赖参数名称简单的确定锁的顺序,因为参数是
     * 具有动态性的,所以,我们改变一下思路,直接根据传入对象的hashCode()大小来
     * 对锁定顺序进行排序(这里要明白的是如何排序不是关键,有序才是关键)。
     *
     * @param from
     * @param to
     * @param money
     */

    @Override
    public void Dotransfer(Account from, Account to, int money) {
        /**
         * 这里需要说明一下为什么不使用HashCode()因为HashCode方法可以被重写,
         * 所以,我们无法简单的使用父类或者当前类提供的简单的hashCode()方法,
         * 所以,我们就使用系统提供的identityHashCode()方法,该方法保证无论
         * 你是否重写了hashCode方法,都会在虚拟机层面上调用一个名为JVM_IHashCode
         * 的方法来根据对象的存储地址来获取该对象的hashCode(),HashCode如果不重写
         * 的话,其实也是通过这个虚拟机层面上的方法,JVM_IHashCode()方法实现的
         * 这个方法是用C++实现的。
         */
        int hashform = System.identityHashCode(from);
        int hashto = System.identityHashCode(to);

        if(hashform>hashto){

            //锁定支出账户
            synchronized (from){
                System.out.println("我拿到了from的锁:"+from.getName());
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (to ){
                    System.out.println("我拿到了to的锁:"+to.getName());
                    from.setMoney(from.getMoney()-money);
                    to.setMoney(to.getMoney()+money);
                    System.out.println("从"+from.getName()+"到"+to.getName()+"的转账动作完成");
                    System.out.println(from.getMoney());
                    System.out.println(to.getMoney());
                }
            }

        }else if(hashform

 

有的朋友会有疑问,这里为什么是if{}else if()else{}一个对象的hashCode()不是不相等吗?我在这里要说的是,hash算法,本来就是一个肯定会出现hash碰撞的算法,在我们Java中hash碰撞的概率是千万分之一左右,但是依然会出现。

 

hashCode()和identityHashCode()

  1. hashCode()方法可以被重写,所以我们在某一个有继承关系的类中是无法确认是否使用的是Object.hashCode()方法,底册通过JVM_IHashCode()方法,使用C++的函数依据该对象的存储地址获取其Hash码。
  2. identityHashCode()这个方法不管你又没有继承关系,有没有重写hashCode()方法,都是底层通过调用JVM_IHashCode()方法来根据对象的存储地址来获取对象的Hash码。
  3. 需要说明的是,不同对象的Hash码是可能相同的,大概是千万分之一左右,但是依然存在,在使用hash码时要特别注意。

 

测试类与前面一样,只是把Transfer_1换成Transfer_2就可以了

 

测试结果:动态死锁消除

死锁,动态死锁,活锁_第6张图片

 

 

 

 场景四,API层面上的可重入锁来消除死锁,导致活锁的出现。

 

转账线程类Transfer_3:

/**
 * 虽然上面解决了,但是我们感觉不够优雅,是否有优雅一点的方式呢?答案是有的
 * 我们下面就来看看使用第二种锁机制来实现的并发安全
 *
 */
public class Transfer_3 extends Thread implements transferAccount{

    private Account from;

    private Account to;

    private int money;


    public Transfer_3(Account from,Account to,int money){
        this.from = from;
        this.to = to;
        this.money = money;
    }


    /**
     * 我们使用了两把API层面的可重入锁,并且都是用的是其可中断的锁等待机制,也就是
     * 说当一个线程持有了一把锁后长时间无法获取另一把锁,就会自动释放其持有的锁,避免
     * 造成死锁。但是很有可能造成活锁问题。
     *
     * @param from
     * @param to
     * @param moeny
     */
    @Override
    public void Dotransfer(Account from, Account to, int moeny) {

        System.out.println("转账动作开始!");

        while (true){
            if(from.getLock().tryLock()){
                try {
                    System.out.println("我拿到了From的锁:"+from.getName());
                    if(to.getLock().tryLock()){
                        try {
                            System.out.println("我拿到了To的锁:"+to.getName());
                            from.setMoney(from.getMoney()-moeny);
                            to.setMoney(to.getMoney()+moeny);
                            System.out.println("转账成功!流程结束!");
                            break;

                        }finally {
                            to.getLock().unlock();
                        }
                    }
                }finally {
                    from.getLock().unlock();
                }
            }

        }
    }

    @Override
    public void run() {
        Dotransfer(from,to,money);
    }
}

上面的代码看起来很优雅,并且使用trylock确实可以有效的解决死锁问题,让我们来测试测试:

 

测试类只需要将Transfer_2换成Transfer_3即可。

死锁,动态死锁,活锁_第7张图片

 

 

活锁的解决:

public class Transfer_3 extends Thread implements transferAccount{

    private Account from;

    private Account to;

    private int money;


    public Transfer_3(Account from,Account to,int money){
        this.from = from;
        this.to = to;
        this.money = money;
    }


    /**
     * 我们使用了两把API层面的可重入锁,并且都是用的是其可中断的锁等待机制,也就是
     * 说当一个线程持有了一把锁后长时间无法获取另一把锁,就会自动释放其持有的锁,避免
     * 造成死锁。但是很有可能造成活锁问题。
     *
     * @param from
     * @param to
     * @param moeny
     */
    @Override
    public void Dotransfer(Account from, Account to, int moeny) {

        System.out.println("转账动作开始!");

        while (true){
            if(from.getLock().tryLock()){
                try {
                    System.out.println("我拿到了From的锁:"+from.getName());
                    if(to.getLock().tryLock()){
                        try {
                            System.out.println("我拿到了To的锁:"+to.getName());
                            from.setMoney(from.getMoney()-moeny);
                            to.setMoney(to.getMoney()+moeny);
                            System.out.println("转账成功!流程结束!");
                            break;

                        }finally {
                            to.getLock().unlock();
                        }
                    }
                }finally {
                    from.getLock().unlock();
                }
            }

            //我们在这里让锁休眠一个随机的时间,目的是错峰(不让它们同时释放,同时获取)。
            try {
                Thread.sleep(new Random().nextInt(10));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void run() {
        Dotransfer(from,to,money);
    }
}

这样其实活锁问题就得到了缓解,但是并没有完全解决,但是对我们程序的并发性的影响已经非常低了,可以接受!

 

 

你可能感兴趣的:(死锁,动态死锁,活锁)