线程死锁问题

多线程时容易出现线程安全问题,其中一种解决方法是使用锁,但是加锁很可能带来另外一个线程安全问题——死锁。

死锁的概念(来自百度百科)

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

死锁产生的条件(来自百度百科)

1)互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。

2)请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。

3)不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。

4)环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。

死锁解决方法

解决死锁最好的方式就是不让死锁产生,也就是说只要不让死锁产生的四个条件同时满足就行了。

1)、确定获取资源(锁)的顺序;

2)、采用尝试获取锁的机制。

简单死锁

例:假设需要同时获得笔和纸才能画画,且现在只有一支笔和一张纸,小明和小芳想画画,我们这里设置了两把锁,所以对应获得笔,锁2对应获得纸,小明和小芳去获取锁,小明获得了锁1(笔),小芳获得了锁2(纸),如果他们都不愿意放弃获得的锁(笔/纸),意味着对方永远无法获得缺少的锁(纸/笔),都不能画画。这就是死锁。

代码:

package com.su.mybatis.oracle.controller;

public class Test {

    //锁1
    private static Object obj1 = new Object();

    //锁2
    private static Object obj2 = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (obj1) {
                    getPen();
                    synchronized (obj2) {
                        getPaper();
                        System.out.println(Thread.currentThread().getName() + ":画画");
                    }
                }
            }
        });
        t1.setName("小明");
        t1.start();

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (obj2) {
                    getPaper();
                    synchronized (obj1) {
                        getPen();
                        System.out.println(Thread.currentThread().getName() + ":画画");
                    }
                }
            }
        });
        t2.setName("小芳");
        t2.start();
    }
    
    public static void getPen() {
        try {
            Thread.sleep(1500);
            System.out.println(Thread.currentThread().getName() + ":获得笔");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    public static void getPaper() {
        try {
            Thread.sleep(1500);
            System.out.println(Thread.currentThread().getName() + ":获得纸");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

程序会一直运行,运行结果:

小明:获得笔
小芳:获得纸

对于这种死锁情况怎么样解决呢?很简单。只需要将小明小芳拿两个锁的顺序改成一致即可。

package com.su.mybatis.oracle.controller;

public class Test {

    //锁1
    private static Object obj1 = new Object();

    //锁2
    private static Object obj2 = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (obj1) {
                    getPen();
                    synchronized (obj2) {
                        getPaper();
                        System.out.println(Thread.currentThread().getName() + ":画画");
                    }
                }
            }
        });
        t1.setName("小明");
        t1.start();

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (obj1) {
                    getPen();
                    synchronized (obj2) {
                        getPaper();
                        System.out.println(Thread.currentThread().getName() + ":画画");

                    }
                }
            }
        });
        t2.setName("小芳");
        t2.start();
    }
    
    public static void getPen() {
        try {
            Thread.sleep(1500);
            System.out.println(Thread.currentThread().getName() + ":获得笔");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    public static void getPaper() {
        try {
            Thread.sleep(1500);
            System.out.println(Thread.currentThread().getName() + ":获得纸");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

输出结果:

小明:获得笔
小明:获得纸
小明:画画
小芳:获得笔
小芳:获得纸
小芳:画画

当然,这里只需要考虑小明和小芳获得锁的顺序,至于获得锁后是获得笔还是获得纸都是不影响的。

动态的死锁

一个经典的问题--转账。

package com.su.mybatis.oracle.controller;

public class Test {

    public static void main(String[] args) {
        User xiaoming = new User("小明", "A1", 20000);//小明,账号A1,20000元
        User xiaofang = new User("小芳", "A2", 15000);//小芳,账号A2,15000元
        Thread t1 = new ThreadTest(xiaoming,xiaofang,3000);//小明转账3000给小芳
        t1.setName("小明");
        t1.start();

        Thread t2 = new ThreadTest(xiaofang,xiaoming,2000);//小芳转账2000给小明
        t2.setName("小芳");
        t2.start();
    }
}

class ThreadTest extends Thread{
    private User sourceUser;
    private User destUser;
    private int money;
    
    public ThreadTest(User sourceUser, User destUser, int money) {
        this.sourceUser = sourceUser;
        this.destUser = destUser;
        this.money = money;
    }
    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        synchronized (sourceUser) {//来源(转出钱的账号)
            try {
                System.out.println(threadName + "获得锁1");
                Thread.sleep(1500);
                sourceUser.setMoney(sourceUser.getMoney() - money);
                synchronized (destUser) {//目的地(转入钱的账号)
                    System.out.println(threadName + "获得锁2");
                    destUser.setMoney(destUser.getMoney() + money);
                    System.out.println(threadName + "转账完成");
                    System.out.println("source:" + sourceUser.toString());
                    System.out.println("dest:" + destUser.toString());
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class User{
    public User(String name, String amount, int money) {
        this.name = name;
        this.amount = amount;
        this.money = money;
    }
    private String name;
    private String amount;
    private int money;
    
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getAmount() {
        return amount;
    }
    public void setAmount(String amount) {
        this.amount = amount;
    }
    public int getMoney() {
        return money;
    }
    public void setMoney(int money) {
        this.money = money;
    }
    
    @Override
    public String toString() {
        return "name:" + name + ",amount:" + amount + ",money:" + money;
    }
}

运行结果:

小明获得锁1
小芳获得锁1

解决方法:使用显示锁中尝试获取锁的机制(方法不止一种)。只有同时获取到两把锁的时候,才执行转账业务。如果获得第一把锁后,没有获取到第二把锁,则释放第一把锁,然后重新尝试。因为存在反复尝试的过程,所以会使用到while 循环,使用break跳出循环,跳出循环的条件是转账完成。

package com.su.mybatis.oracle.controller;

import java.util.Random;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Test {

    public static void main(String[] args) {
        User xiaoming = new User("小明", "A1", 20000);// 小明,账号A1,20000元
        User xiaofang = new User("小芳", "A2", 15000);// 小芳,账号A2,15000元
        Thread t1 = new ThreadTest(xiaoming, xiaofang, 3000);// 小明转账3000给小芳
        t1.setName("小明");
        t1.start();

        Thread t2 = new ThreadTest(xiaofang, xiaoming, 2000);// 小芳转账2000给小明
        t2.setName("小芳");
        t2.start();
    }
}

class ThreadTest extends Thread {
    private User sourceUser;
    private User destUser;
    private int money;

    public ThreadTest(User sourceUser, User destUser, int money) {
        this.sourceUser = sourceUser;
        this.destUser = destUser;
        this.money = money;
    }

    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        while (true) {
            if (sourceUser.getLock().tryLock()) {// 来源(转出钱的账号)
                System.out.println(threadName + "获得锁1");
                try {
                    if (destUser.getLock().tryLock()) {// 目的地(转入钱的账号)
                        try {
                            sourceUser.setMoney(sourceUser.getMoney() - money);
                            destUser.setMoney(destUser.getMoney() + money);
                            System.out.println(threadName + "转账完成");
                            System.out.println("source:" + sourceUser.toString());
                            System.out.println("dest:" + destUser.toString());
                            break;
                        } finally {
                            destUser.getLock().unlock();
                        }
                    }
                } finally {
                    sourceUser.getLock().unlock();
                }
            }
            try {
                Thread.sleep( new Random().nextInt(5));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class User {
    public User(String name, String amount, int money) {
        this.name = name;
        this.amount = amount;
        this.money = money;
    }

    private String name;
    private String amount;
    private int money;

    private final Lock lock = new ReentrantLock();

    public Lock getLock() {
        return lock;
    }

    public String getName() {
        return name; 
    }

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

    public String getAmount() {
        return amount;
    }

    public void setAmount(String amount) {
        this.amount = amount;
    }

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }

    @Override
    public String toString() {
        return "name:" + name + ",amount:" + amount + ",money:" + money;
    }
}

输出结果:

小明获得锁1
小芳获得锁1
小芳转账完成
source:name:小芳,amount:A2,money:13000
dest:name:小明,amount:A1,money:22000
小明获得锁1
小明转账完成
source:name:小明,amount:A1,money:19000
dest:name:小芳,amount:A2,money:16000

Thread.sleep( new Random().nextInt(5))的作用:

让线程进行短暂休眠,使它们错开拿锁时间,减少尝试拿锁过程中的碰撞,杜绝活锁的产生,避免资源(eg:CPU等)的浪费。

活锁:并发编程中,线程没有被阻塞,因为某些必要条件不满足,一直处于尝试获取资源(eg:锁)→获取资源失败→尝试获取资源...的过程。活锁状态的线程会不断的改变状态,需要消耗一定的资源,活锁有可能自行解开(将Thread.sleep( new Random().nextInt(5))注掉运行即可复现)。

 

 

如果有写的不对的地方,请大家多多批评指正,非常感谢!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值