协作对象死锁及其解决方案

news2024/10/2 22:29:17

协作对象死锁及其解决方案

1.前言

在遇到转账等的需要保证线程安全的情况时,我们通常会使用加锁的方式来保证线程安全,但如果无法合理的使用锁,很可能导致死锁。或者有时我们使用线程池来进行资源的使用,如调用数据库,但无法合理使用锁也可能导致死锁。
Java程序无法自动检测死锁并预防,在Java程序中如果遇到死锁将会是一个非常严重的问题,轻则导致线程阻塞,程序响应时间变长,系统吞吐量变小;重则导致系统部分功能失去响应能力无法使用。因此我们应当预防和规避这些问题。

如果对Synchronized关键字的基本使用方法不是很清楚的话可以看一下这篇文章:
Java中Synchronized关键字的基本使用方法

2.死锁说明

死锁是指两个或多个线程在同一资源上相互占用,并请求锁定对方的资源,从而导致恶性循环的现象。
死锁产生的四个必要条件:
1)互斥条件:资源是独占且排他的,即任意时刻一个资源只能给一个线程使用。其他线程若申请一个资源,而该资源被另一线程占用时,则申请者只能等待,直到资源被占用者释放。
2)不可剥夺条件:线程所获得的资源在未使用完毕之前,不会被其他线程强行剥夺,而只能由获得该资源的线程进行释放。
3)请求和保持条件:线程每次申请它所需要的一部分资源,在申请新的资源的同时,继续占用已分配到的资源。
4)循环等待条件:在发生死锁时必然存在一个线程等待队列{P1,P2,…,Pn},其中P1等待P2占有的资源,P2等待P3占有的资源,…,Pn等待P1占有的资源,形成一个线程等待环路,环路中每一个线程所占有的资源同时被另一个申请。

协作对象死锁

死锁的产生往往不像顺序死锁那样明显(关于顺序死锁可以看一下这本篇文章:顺序死锁及其解决方案),就算其存在死锁风险往往也只会在高并发的场景下才可能暴露出来(这并不意味着应用没有高并发就不用考虑死锁问题了,作为开发者,你无法预测用户的操作)。
接下来介绍一种隐藏的比较深的死锁,这种死锁往往产生于多个协作对象的函数调用不透明。

Coordinate:坐标类,记录出租车的经度和纬度。
Fleet: 出租车车队类,车队类包含两个集合:车队中所有车辆信息taxis和车队中当前空闲的出租车信息available,此外还提供获取车队中所有出租车当前地址信息快照的方法getImage()
Taxi:出租车类,出租车属于某个出租车车队Fleet,此外还包含当前坐标location和目的地坐标destination,出租车在更新目的地信息的时候会判断当前坐标与目的地坐标是否相等,相等则会通知所属车队车辆空闲,可以接收下一个目的地
Image: 车辆地址信息快照类,用于获取出租车的地址信息

/**
 * 坐标类
 */
public class Coordinate {

    /**
     * 经度
     */
    private Double longitude;

    /**
     * 纬度
     */
    private Double latitude;

    public Coordinate(Double longitude, Double latitude) {
        this.longitude = longitude;
        this.latitude = latitude;
    }

    public Double getLongitude() {
        return longitude;
    }

    public void setLongitude(Double longitude) {
        this.longitude = longitude;
    }

    public Double getLatitude() {
        return latitude;
    }

    public void setLatitude(Double latitude) {
        this.latitude = latitude;
    }
    
    @Override
    public String toString() {
        return "Coordinate{" +
                "longitude=" + longitude +
                ", latitude=" + latitude +
                '}';
    }
    
}
/**
 * 车队类 -> 调度管理出租车
 */
public class Fleet {

    /**
     * 车队中所有出租车
     */
    private final Set<Taxi> taxis;
    /**
     * 车队中目前空闲的出租车
     */
    private final Set<Taxi> available;

    public Fleet(Set<Taxi> taxis) {
        this.taxis = this.available = taxis;

    }

    /**
     * 出租车到达目的地后调用该方法,向车队发出当前出租车空闲信息
     *
     * @param taxi
     */
    public synchronized void free(Taxi taxi) {
        System.out.println("出租车到站了");
        available.add(taxi);
    }

    /**
     * 获取所有出租车在不同时刻的地址快照
     *需要获取当前车队Fleet的锁,以及在遍历出租车获取其地址信息时需要获取每个出租车Taxi对象的锁
     * @return
     */
    public synchronized Image getImage() {
        Image image = new Image();
        for (Taxi taxi : taxis) {
            image.drawMarker(taxi);
        }
        return image;
    }

}
/**
 * 出租车类
 */
public class Taxi {

    /**
     * 出租车唯一标志
     */
    private String id;
    /**
     * 当前坐标
     */
    private Coordinate location;
    /**
     * 目的地坐标
     */
    private Coordinate destination;
    /**
     * 所属车队
     */
    private final Fleet fleet;

    /**
     * 获取当前地址信息
     *
     * @return
     */
    public synchronized Coordinate getLocation() {
        return location;
    }

    /**
     * 更新当前地址信息
     * 如果当前地址与目的地地址一致,则表名到达目的地需要通知车队,当前出租车空闲可用前往下一个目的地
     *需要获取当前出租车Taxi对象的锁以及出租车所属车队Fleet的锁
     * @param location
     */
    public synchronized void setLocation(Coordinate location) {
        this.location = location;
        if (location.equals(destination)) {
            fleet.free(this);
        }
    }

    public Coordinate getDestination() {
        return destination;
    }

    /**
     * 设置目的地
     *
     * @param destination
     */
    public synchronized void setDestination(Coordinate destination) {
        this.destination = destination;
    }

    public Taxi(Fleet fleet) {
        this.fleet = fleet;
    }

    public String getId() {
        return id;
    }

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

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        Taxi taxi = (Taxi) o;
        return Objects.equals(location, taxi.location) &&
                Objects.equals(destination, taxi.destination);
    }

    @Override
    public int hashCode() {
        return Objects.hash(location, destination);
    }
}
/**
 * 获取所有出租车在某一时刻的位置快照
 */
public class Image {

    Map<String, Coordinate> locationSnapshot = new HashMap<>();

    public void drawMarker(Taxi taxi) {
        locationSnapshot.put(taxi.getId(), taxi.getLocation());
        System.out.println("出租车当前位置为:" + taxi.getLocation().toString());
    }

}
public class TaxiDeadLock {

    public static void main(String[] args) {
       HashSet<Taxi> taxiList = new HashSet<Taxi>();
        Fleet fleet = new Fleet(taxiList);
        Taxi taxi1 = new Taxi(fleet);
        taxiList.add(taxi1);
        Coordinate coordinate = new Coordinate(1.1, 2.2);
        taxi1.setDestination(coordinate);
        taxi1.setLocation(new Coordinate(1.0, 2.0));
        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                fleet.getImage();
            }, "车队").start();
            new Thread(() -> {
                taxi1.setLocation(coordinate);
            }, "出租车").start();
        }
    }

}

在这里插入图片描述

在这里插入图片描述
从图片上我们可以看出,出租车和车队互相锁住了对方所需要的资源,并且请求对方所占有的资源,进程进入了死锁状态。
让我们从代码层面上看:
车队调用getImage()方法获取所有出租车在不同时刻的地址快照时,会先锁住当前车队,之后遍历出租车集合获取每辆出租车地址信息,通过drawMarker(Taxi taxi)方法获取出租车地址信息,这时我们会调用taxi.getLocation()方法,此方法会锁住当前出租车。
出租车调用setLocation(Coordinate location)方法更新当前地址信息时,会先锁住当前出租车,之后当前地址与目的地地址进行判断,如果一致,则调用free(Taxi taxi)方法通知车队当前出租车空闲,此方法会锁住出租车所属的车队。
以上代码并非显式的在一个方法中对多个资源进行加锁,而是隐式的在多个类中对对多个方法进行加锁,粗看之下似乎没什么问题,但是细细分析,就会发现存在很大的死锁隐患。

使用同步方法可能会发生较长时间的阻塞,这就可能给了死锁的机会。基于此类问题,我们通常要对需求进行分析,然后尝试采用缩小锁的范围或者不加锁等方式来解决。

下面让我们分析一下上文代码中的死锁:车队获取所有出租车在不同时刻的地址快照时,是一定要加锁的,因为这并不是一个原子性操作,不加锁就有可能发生车队中原本是5辆车,循环的时候突然多了或者少了车的情况。出租车更新当前地址信息时同理。所以这两处的锁,我们是不可以去掉的。那我们只可以尝试采用缩小锁的范围来防范:

/**
 * 车队类 -> 调度管理出租车
 */
public class Fleet {

    /**
     * 车队中所有出租车
     */
    private final Set<Taxi> taxis;
    /**
     * 车队中目前空闲的出租车
     */
    private final Set<Taxi> available;

    public Fleet(Set<Taxi> taxis) {
        this.taxis = this.available = taxis;

    }

    /**
     * 出租车到达目的地后调用该方法,向车队发出当前出租车空闲信息
     *
     * @param taxi
     */
    public synchronized void free(Taxi taxi) {
        System.out.println("出租车到站了");
        available.add(taxi);
    }

    /**
     * 优化内容
     * getImage()不再是同步方法
     * 将同步范围(锁住的代码)缩小
     * this(出租车车队对象)与drawMarker()方法中获取taxi对象的锁不再嵌套不会死锁
     *
     * @return
     */
    public Image getImage() {
        Set<Taxi> copy;
        synchronized (this) {
            copy = new HashSet<Taxi>(taxis);
        }
        Image image = new Image();
        for (Taxi taxi : copy) {
            image.drawMarker(taxi);
        }
        return image;
    }

}
/**
 * 出租车类
 */
public class Taxi {

    /**
     * 出租车唯一标志
     */
    private String id;
    /**
     * 当前坐标
     */
    private Coordinate location;
    /**
     * 目的地坐标
     */
    private Coordinate destination;
    /**
     * 所属车队
     */
    private final Fleet fleet;

    /**
     * 获取当前地址信息
     *
     * @return
     */
    public synchronized Coordinate getLocation() {
        return location;
    }

    /**
     * 优化内容
     * setLocation(Coordinate location)方法不在加锁
     * 将同步范围(锁住的代码)缩小
     * this的锁与fleet顺序获取 ,锁内没有嵌套,不会死锁
     * @param
     */
    public void setLocation(Coordinate location) {
        this.location = location;
        boolean release = false;
        synchronized (this) {
            if (location.equals(destination)) {
                release = true;
            }
        }
        if (release) {
            fleet.free(this);
        }
    }

    public Coordinate getDestination() {
        return destination;
    }

    /**
     * 设置目的地
     *
     * @param destination
     */
    public synchronized void setDestination(Coordinate destination) {
        this.destination = destination;
    }

    public Taxi(Fleet fleet) {
        this.fleet = fleet;
    }

    public String getId() {
        return id;
    }

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

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        Taxi taxi = (Taxi) o;
        return Objects.equals(location, taxi.location) &&
                Objects.equals(destination, taxi.destination);
    }

    @Override
    public int hashCode() {
        return Objects.hash(location, destination);
    }
}

在这里插入图片描述
从上文代码中可以看出,我们优化了车队类的getImage()方法,将其同步方法改为了同步代码块,缩小了锁的范围:当调用getImage()方法时,我们创建一个车队拷贝,然后上锁,确保车队拷贝和当前车队一致,之后解锁,用车队拷贝去循环获取出租车的锁。之前会出现死锁是因为车队类既锁住了自己又去请求出租车的锁,现在请求出租车的时候没有锁住自己,破坏了死锁的四个触发条件之一,构不成死锁,出租车类的方法同理。
简单的说车队类和出租车类的方法我们只要修改一个就破坏了死锁的条件,无法构成死锁,但是我们还是需要对其不合理的代码进行优化。

由此我们可以得出结论:对于协作对象死锁,我们通常要对需求进行分析,然后尝试采用缩小锁的范围或者不加锁等方式来破坏死锁的四个必须条件中的一个或多个来解决。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/369678.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Swagger狂神学习笔记

学习目标&#xff1a; 了解Swagger的概念及作用 掌握在项目中集成Swagger自动生成API文档 前后端分离 前端 -> 前端控制层、视图层 后端 -> 后端控制层、服务层、数据访问层 前后端通过API进行交互 前后端相对独立且松耦合 产生的问题 前后端集成&#xff0c;前端或…

支持U盘数据、误删文件、硬盘数据 、回收站数据恢复的软件

好用的Windows数据恢复软件的标准 在数字和信息经济时代&#xff0c;数据是必不可少的。没有人可以承受由于意外删除、格式化和其他原因导致数据丢失的风险。与其在数据恢复服务上花费大量资金或花费大量时间努力自己取回数据&#xff0c;用户更喜欢使用Windows数据恢复软件…

Ask林曦|来回答,30个你关心的日常问题(一)

在林曦老师的线上书法直播课上&#xff0c;上课前后的聊天时间里&#xff0c;时常有同学向林曦老师提问&#xff0c;这些问题涵盖了日常生活的诸多方面&#xff0c;从身体的保养&#xff0c;到快乐的法门&#xff0c;皆是大家感兴趣的&#xff0c;也都共同关切的。      暄…

破解票房之谜:为何高票房电影绕不过“猫眼们”?

如此火爆的春节档很多&#xff0c;如此毁誉参半的春节档鲜有。2023开年&#xff0c;集齐张艺谋、沈腾的《满江红》&#xff0c;以及有票房前作打底的《流浪地球2》接连两部春节档电影票房进入前十&#xff0c;为有些颓靡的中国电影市场注入了一针“强心剂”。与票房同样热闹起来…

git实战技巧-本地刚做出的修改、暂存和提交如何进行撤销

1、解决思路工作区和暂存区内容的撤销&#xff0c;直接按照对应命令或者IDEA界面完成操作就行。对于已提交的撤销回滚建议如下&#xff1a;1. 代码如果仅仅是回撤到指定版本&#xff0c;该版本之上的更新是不需要的&#xff0c;选择Hard模式。2. 代码回撤到指定版本时&#xff…

父传子与子传父步骤

父传子&#xff1a; 问题&#xff1a;父页面中引入子组件 把想要传给子页面的值用在子组件中用 &#xff1a;值“值” (用同一个值好区分)来绑定。 在子页面中用props接收 子组件不能改变父组件传过来的值。&#xff08;传多个页面的时候是&#xff0c;比如父传孙的时候我会…

2020蓝桥杯真题门牌制作(填空题) C语言/C++

题目描述 本题为填空题&#xff0c;只需要算出结果后&#xff0c;在代码中使用输出语句将所填结果输出即可。 小蓝要为一条街的住户制作门牌号。 这条街一共有 2020 位住户&#xff0c;门牌号从 1 到2020 编号。 小蓝制作门牌的方法是先制作 0 到 9 这几个数字字符&#xff…

两款全新模式——代言人、合伙人的玩法分享

近几年来&#xff0c;伴随着技术创新与时代发展趋势&#xff0c;线上消费订单配送服务高效率变得更加高&#xff0c;生活节奏的加快也使人们对于消费及时性越来越注重&#xff0c;及时要求已经大爆发。 用户的消费方式也发生了翻天覆地的变化&#xff0c;消费者跟商家不再是单…

jvm常识

Jvm工作原理学习笔记0126一、JVM的生命周期1.JVM实例对应了一个独立运行的java程序它是进程级别a)启动。启动一个Java程序时&#xff0c;一个JVM实例就产生了&#xff0c;任何一个拥有public static void main(String[] args)函数的class都可以作为JVM实例运行的起点b)运行。ma…

Revit中如何使管道垂直连接及【支管升降】

一、Revit中如何使管道垂直连接 在管道的标高与所要相连的管子相差无几的时候&#xff0c;拉动管道所产生的管道连接方式往往不符合应有的设计思路&#xff0c;如图1所示&#xff1a; 1)相连之后所生成的一个布线方案&#xff0c;但不是我们所想要的布线方案。如图2所示&#x…

PPP点到点协议认证之PAP认证

PPP点到点协议认证之PAP认证 需求 如图配置接口的IP地址将R1配置为认证端&#xff0c;用户名和密码是 huawei/hcie &#xff0c;使用的认证方式是pap确保R1和R2之间可以互相ping通 拓扑图 配置思路 确保接口使用协议是PPP确保接口的IP地址配置正确在R1 的端口上&#xff0c…

【LeetCode】剑指 Offer 14- I. 剪绳子 p96 -- Java Version

题目链接&#xff1a;https://leetcode.cn/problems/jian-sheng-zi-lcof/ 1. 题目介绍&#xff08;14- I. 剪绳子&#xff09; 给你一根长度为 n 的绳子&#xff0c;请把绳子剪成整数长度的 m 段&#xff08;m、n都是整数&#xff0c;n>1并且m>1&#xff09;&#xff0c…

【Mysql】InnoDB引擎

【Mysql】InnoDB引擎 文章目录【Mysql】InnoDB引擎1. 逻辑存储结构2. 架构2.1 概述2.2 内存结构2.2.1 Buffer Pool(缓冲池)2.2.2 Change Buffer(更改缓冲区)2.2.3 Adptive Hash Index(自适应hash索引)2.2.4 Log Buffer(日志缓冲区)2.3 磁盘结构2.3.1 System Tablespace(系统表空…

图解LeetCode——剑指 Offer 42. 连续子数组的最大和

一、题目 输入一个整型数组&#xff0c;数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。要求时间复杂度为O(n)。 二、示例 2.1> 示例1: 【输入】 nums [-2,1,-3,4,-1,2,1,-5,4] 【输出】 6 【解释】 连续子数组 [4,-1,2,1] 的和最大&#xff0c;…

鉴源论坛 · 观擎丨民机机载软件中的开发和验证工具

作者 |蔡喁 上海控安可信软件创新研究院副院长 版块 | 鉴源论坛 观擎 01 工具鉴定 现代软件工程中&#xff0c;工具已经出现在软件研制过程中的各个环节中。在常见的软件研制环节中&#xff0c;使用工具包括软件需求工具、软件设计工具、软件架构工具和软件测试工具等。这…

【C++】模板与STL库

STL&#xff08;标准模板库&#xff09; 文章目录模板STL模板 模板是泛型编程的基础&#xff0c;泛型编程以一种独立于任何特定类型的方式编写代码。 模板是创建泛型类或函数的蓝图或公式。库容器&#xff0c;比如迭代器和算法&#xff0c;都是泛型编程的例子&#xff0c;它们…

线性判别分析仅在线性可分数据上能获得理想结果

Q: 线性判别分析仅在线性可分数据上能获得理想结果, 试设计一个改进方法, 使其能较好地用于飞线性可分数据 The same for “Linear discriminant analysis can only obtain ideal results on linearly separable data. Try to design an improved method to make it better us…

Vue3:自定义指令以及简单的后台管理权限封装

目录 前言&#xff1a; 自定义指令介绍&#xff1a; 局部的自定义指令&#xff1a; 全局自定义指令&#xff1a; 讲讲后台管理权限管理&#xff1a; 前言&#xff1a; 说起这个自定义指令的使用场景&#xff0c;我第一反应就是&#xff0c;后台管理的权限管理&#xff0c;要…

VBA学习——1

VBA 过程的基本语法如下&#xff1a; Sub 过程名(参数)语句1语句2...End SubVBA 函数与 VBA 过程很相似&#xff0c;除了使用的关键词外&#xff0c;主要区别是&#xff0c;函数可以返回值。 Function [函数名]([参数]) As [返回值类型]语句1 语句2 ... 语句n [函…

Servlet笔记(9):Cookie处理

一、Cookies处理 1、Cookies概念 Cookies是存储在客户端计算机上的文本文件&#xff0c;并保留各种跟踪信息。 识别返回用户的三个步骤 服务器脚本向浏览器发送一组Cookies。例如姓名、年龄或识别号码等。浏览器将这些信息存储在本地计算机上。当下一次浏览器向Web服务器发送…