死锁 活锁 饥饿 出现原因及解决方案

死锁

概念

死锁:一组互相竞争资源的线程因互相等待,导致“永久”阻塞的现象。
说白了就是:两个线程互相持有对方所需的资源,互不释放且互相等待

死锁示例
/**
 * 死锁
 *
 * @author m
 */
public class Account {

    private int balances;

    public static void main(String[] args) {
        Account A = new Account();
        Account B = new Account();
        new Thread(() -> {
            A.transfer(B, 100);
        }, "线程1 ").start();
        new Thread(() -> {
            B.transfer(A, 100);
        }, "线程2 ").start();
    }

    //转账
    public void transfer(Account target, int money) {
    	//休眠(放大问题发生性)
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (this) {
            System.out.println(Thread.currentThread().getName() + " 持有锁 " + this + ",等待锁" + target);
            synchronized (target) {
                System.out.println(Thread.currentThread().getName() + " 持有锁 " + target);
                this.balances += money;
                target.balances -= money;
                System.out.println("转账结束");
            }
        }
    }


}

执行结果
在这里插入图片描述

为什么会出现死锁呢?

假设线程T1执行转账A->B,线程T2执行转账B->A,两个线程同时执行synchronized (this)时,线程T1获取了A的锁,线程T2获取了B的锁,同时又执行到了synchronized (target)时,线程T1获取B锁时,发现B锁已经被线程T2持有,线程T1进入阻塞状态;与此同时,线程T2获取A锁时,发现A锁已经被线程T1持有,线程T2也进入阻塞状态。参考下图理解
在这里插入图片描述

如何解决死锁呢?

并发程序一旦死锁,一般没有特别好的方法,很多时候我们只能重启应用。因此,解决死锁问题最好的办法还是规避死锁

那如何避免死锁呢?要避免死锁就需要分析死锁发生的条件,有个叫 Coffman 的牛人早就总结过了,只有以下这四个条件都发生时才会出现死锁

  1. 互斥,共享资源 X 和 Y 只能被一个线程占用;
  2. 占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;
  3. 不可抢占,其他线程不能强行抢占线程 T1 占有的资源;
  4. 循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。

既然知道了出现死锁的必要条件,其实只要破坏其中一条就可避免死锁

其中,互斥这个条件我们没有办法破坏,因为我们用锁为的就是互斥。不过其他三个条件都是有办法破坏掉的,到底如何做呢?

  1. 对于“占用且等待”这个条件,我们可以一次性申请所有的资源,这样就不存在等待了。
  2. 对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。
  3. 对于“循环等待”这个条件,可以按顺序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后自然就不存在循环了。
解决死锁代码实现

下面我只列出避免死锁的第三种方案按照顺序加锁,同时也是成本最低的

//转账
    public void transfer(Account target, int money) {
        Account little;     //id小的账户
        Account big;        //id大的账户
        if (this.id < target.id) {
            little = this;
            big = target;
        } else {
            little = target;
            big = this;
        }
        // 锁定id小的账户
        synchronized (little) {
            System.out.println(Thread.currentThread().getName() + " 持有锁 " + this + ",等待锁" + target);
            // 锁定id大的账户
            synchronized (big) {
                System.out.println(Thread.currentThread().getName() + " 持有锁 " + target);
                this.balances += money;
                target.balances -= money;
                System.out.println("转账结束");
            }
        }
    }

活锁

概念

活锁:有时线程虽然没有发生阻塞,但仍然会存在执行不下去的情况。
说白了就是:两个线程因互相礼让,导致线程永远的礼让下去

例如:可以类比现实世界里的例子,路人甲从左手边出门,路人乙从右手边进门,两人为了不相撞,互相谦让,路人甲让路走右手边,路人乙也让路走左手边,结果是两人又相撞了。这种情况,基本上谦让几次就解决了,因为人会交流啊。可是如果这种情况发生在编程世界了,就有可能会一直没完没了地“谦让”下去,成为没有发生阻塞但依然执行不下去的“活锁”。

活锁示例:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 活锁
 *
 * @author m
 */
public class Account2 {

    private int balances;

    private final Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        Account2 A = new Account2();
        Account2 B = new Account2();
        new Thread(() -> {
            A.transfer(B, 100);
        }, "线程1 ").start();
        new Thread(() -> {
            B.transfer(A, 100);
        }, "线程2 ").start();
    }

    void transfer(Account2 target, int money) {
        while (true) {
            if (this.lock.tryLock()) {
                System.out.println(Thread.currentThread().getName() + " 持有锁 " + this.lock + ",等待锁" + target.lock);
                try {
                    if (tar.lock.tryLock()) {
                        System.out.println(Thread.currentThread().getName() + " 持有锁 " + target.lock);
                        try {
                            this.balances -= money;
                            tar.balances += money;
                        } finally {
                            tar.lock.unlock();
                        }
                    }
                } finally {
                    this.lock.unlock();
                }
            }
        }
    }


}
如何解决活锁呢?

谦让时,尝试等待一个随机的时间就可以了。“等待一个随机时间”的方案虽然很简单,却非常有效,Raft 这样知名的分布式一致性算法中也用到了它。

例如上面的那个例子,路人甲走左手边发现前面有人,并不是立刻换到右手边,而是等待一个随机的时间后,再换到右手边;同样,路人乙也不是立刻切换路线,也是等待一个随机的时间再切换。由于路人甲和路人乙等待的时间是随机的,所以同时相撞后再次相撞的概率就很低了。

饥饿

概念

饥饿:线程因无法访问所需资源而无法执行下去的情况
说白了就是:假设有1万个线程,还没等前面的线程执行完,后面的线程就饿死了

如何解决饥饿呢?

下面提供了三种方案

  1. 保证资源充足
  2. 公平地分配资源
  3. 避免持有锁的线程长时间执行

这三个方案中,方案一和方案三的适用场景比较有限,因为很多场景下,资源的稀缺性是没办法解决的,持有锁的线程执行的时间也很难缩短。倒是方案二的适用场景相对来说更多一些。

开心一刻

两个老人去养老院。。。
70岁的老人进去了,90岁老人没进去。
工作人员:“对不起,大爷,我们不接受儿女健在的老人。您的资料显示,你有一个儿子。”
90岁老人:“操,刚刚进去的就是我儿子! ”
在这里插入图片描述

如果觉得不错,帮忙点个赞,您的点赞将是我的动力!

  • 5
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值