通过银行转账问题解说死锁解决方案

大家都知道,在并发情况下对两个账户进行转账操作可能会产生死锁,可能出现死锁的原因是,并发情况下对两个账户的操作无法保证其执行顺序。

1. 并发问题描述

假如现在执行下面的操:

线程一执行的是:【账户A】给【账户B】转账

线程二执行的是:【账户B】给【账户A】转账

如果两个转账动作同时执行,则会出现线程一会请求对【账户B】进行加锁,线程二会请求对【账户A】进行加锁

由于此时的【账户A】已由线程一进行锁定,【账户B】已由线程二进行锁定 此时就会产生死锁问题。接下来分析一下产生死锁的原因,以及如何避免死锁。

 

2. 如何避免死锁

有个叫 Coffman 的牛人总结过一条经验,只有当以下四个条件同时发生,才会出现死锁,所以只要打破其中一个条件,就可以避免死锁:

  • 互斥,共享资源 X 和 Y 只能被一个线程占用
  • 占有且等待,线程 A 获取到资源 X,在等待资源 Y 的时候,不释放资源 X
  • 不可抢占,其他线程不能强行抢占线程 A 占有的资源
  • 循环等待,线程 A 等待线程 B 占有的资源,线程 B 等待线程 A 的资源

首先,互斥这个条件是没法破坏的,因为锁存在的目的就是互斥,对于剩下的三个条件都可破坏。

2.1 破坏占有且等待

对于占有且等待,可以同时获取要使用的多个资源锁X和Y,这样就不会存在取得了X还要等待Y。这种方式只在需要获取的资源锁较少的情况下使用,如果要获取的资源锁很多(例如10个),就不太可行。代码实现时,我们通过增加一个 Allocator 账号管理员对象,并且将其设置为单例,每次进行转账的时候,我们都先通过 Allocator 分配账号,如果分配账号成功,则进行转账,如果失败则重新获取,可以设置一个失败次数或是超时时间,达到失败次数或超时时间则转账失败。如下是代码实现,

package com.zhouj.endless.diy;

import java.util.ArrayList;
import java.util.List;

/**
 * @author 天启
 * @date 2020-02-19 16:31
 * @description 账户分配
 */
public class Allocator {

    private static class InstanceHolder {
        static Allocator instance = new Allocator();
    }

    public static Allocator getInstance() {
        return InstanceHolder.instance;
    }

    /**
     * 上锁的账户列表
     */
    private List<Account> lockAccountList = new ArrayList<>();

    /**
     * 申请分配账户
     *
     * @param from 从这个账户转钱
     * @param to   转钱到这个账户
     */
    public synchronized void apply(Account from, Account to) {
        while (lockAccountList.contains(from) || lockAccountList.contains(to)) {
            // 如果两个账户中,只要有一个账户上锁了,则申请失败,进入循环等待
            try {
                // 阻塞当前线程,等待通知
                this.wait();
            } catch (InterruptedException e) {
            }
        }
        lockAccountList.add(from);
        lockAccountList.add(to);
    }

    /**
     * 释放账户锁
     *
     * @param from 从这个账户转钱
     * @param to   转钱到这个账户
     */
    public synchronized void free(Account from, Account to) {
        lockAccountList.remove(from);
        lockAccountList.remove(to);
        // 通知所有线程,让其再次获取锁
        this.notifyAll();
    }

    private Allocator() {
    }
}

2.2 破坏不可抢占条件

对于不可抢占,可以获取了部分资源,再进一步获取其他资源时如果获取不到时,把已经获取的资源一起释放掉。破坏不可抢占条件,需要线程在获取不到锁的情况下主动释放它拥有的资源。当我们使用synchronized的时候,线程是没有办法主动释放它占有的资源的。因为,synchronized在申请不到资源时,会使线程直接进入阻塞状态,而线程进入了阻塞状态就不能主动释放占有的资源。java.util.concurrent中的Lock接口,提供了如下三种设计思想都可以解决死锁的不可抢占条件:

  1. 能够响应中断:线程处于阻塞状态时可以接收中断信号。我们便可以给阻塞的线程发送中断信号,唤醒线程,线程便有机会释放它曾经拥有的锁。这样便可破坏不可抢占条件。
  2. 支持超时:如果线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个错误,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
  3. 非阻塞地获取锁:如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线程也有机会释放曾经持有的锁。这样也可以破坏不可抢占条件。

对应方法

// 支持中断的 API
void lockInterruptibly() throws InterruptedException;

// 支持超时的 API
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

// 支持非阻塞获取锁的 API
boolean tryLock();

使用非阻塞的获取锁实现:

package com.zhouj.endless.diy;

/**
 * @author 天启
 * @date 2020-02-19 17:07
 * @description 可抢占
 */
public class Preemptible {
    public boolean transferMoney(Account fromAcct, Account toAcct) {
        while (true) {
            // 使用tryLock()获取锁
            if (fromAcct.lock.tryLock()) {
                try {
                    // 使用tryLock()获取锁
                    if (toAcct.lock.tryLock()) {
                        return true;
                    }
                } finally {
                    // 释放前面获取到的锁
                    fromAcct.lock.unlock();
                }
            }
        }
    }
}

 

2.3 破坏循环等待条件

对于循环等待,可以将需要获取的锁资源排序,按照顺序获取,这样就不会多个线程交叉获取相同的资源导致死锁,而是在获取相同的资源时就等待,直到它释放。比如根据账号的主键 id 进行排序,从小到大的获取锁,这样就可以避免循环等待。

package com.zhouj.endless.diy;

/**
 * @author 天启
 * @date 2020-02-19 16:31
 * @description 账户排序
 */
public class Account {

    private Integer id;

    private int balance;

    /**
     * 转账
     *
     * @param target 目标
     * @param amt    金额
     */
    void transfer(Account target, int amt) {
        Account left = this;
        Account right = target;
        if (this.id > target.id) {
            left = target;
            right = this;
        }
        // 锁定序号小的账户
        synchronized (left) {
            // 锁定序号大的账户
            synchronized (right) {
                if (this.balance > amt) {
                    this.balance -= amt;
                    target.balance += amt;
                }
            }
        }
    }
}

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值