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

news/2024/5/20 13:00:41 标签: java, jvm, 死锁, 协作对象死锁

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

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: 车辆地址信息快照类,用于获取出租车的地址信息

java">/**
 * 坐标类
 */
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 +
                '}';
    }
    
}
java">/**
 * 车队类 -> 调度管理出租车
 */
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;
    }

}
java">/**
 * 出租车类
 */
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);
    }
}
java">/**
 * 获取所有出租车在某一时刻的位置快照
 */
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());
    }

}
java">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辆车,循环的时候突然多了或者少了车的情况。出租车更新当前地址信息时同理。所以这两处的锁,我们是不可以去掉的。那我们只可以尝试采用缩小锁的范围来防范:

java">/**
 * 车队类 -> 调度管理出租车
 */
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;
    }

}
java">/**
 * 出租车类
 */
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.niftyadmin.cn/n/95805.html

相关文章

18. linux系统基础

shell 命令解析器 命令解析器作用&#xff1a; 他把在终端上输出的命令 给你解析成内核可以识别的指令&#xff0c;内核 是经过命令解析器的加工 shell在找命令的时候&#xff0c;所包含的路径&#xff0c;就是在这些路径里去 找 找到就执行 找不到就报错 报错 要么 这个命…

搭建直播平台服务器什么配置最合适?直播平台专用服务器(驰网i9-13900k服务器)

如今&#xff0c;视频直播越来越受欢迎&#xff0c;视频和直播平台也越来越多&#xff0c;直播平台和视频网站都需要更好的服务器来支持。那么&#xff0c;视频直播平台需要什么服务器呢&#xff1f;以一个简单的直播网站为例。如果每天在线人数约1000人&#xff0c;同时在线人…

Linux基础命令-find搜索文件位置

文章目录 find 命令介绍 语法格式 命令基本参数 参考实例 1&#xff09;在root/data目录下搜索*.txt的文件名 2&#xff09;搜索一天以内最后修改时间的文件&#xff1b;并将文件删除 3&#xff09;搜索777权限的文件 4&#xff09;搜索一天之前变动的文件复制到test…

为什么HR眼中,Python是真正的简历加分项?

教育部在发布的关于《2023届高校毕业生预计1158万 校园招聘月启动》文中明确指出&#xff1a;“2023届高校毕业生预计1158万&#xff0c;同比增加82万人”。除开考研、考公的少数同学&#xff0c;几百万大军拼命往大企业投简历&#xff0c;求职竞争十分激烈。 来源&#xff1a…

十五、多路查找树

1、二叉树与B树 1.1 二叉树的问题分析 二叉树的操作效率较高&#xff0c;但是也存在问题&#xff0c;请看下面的二叉树 二叉树需要加载到内存中&#xff0c;如果二叉树的节点少&#xff0c;没有什么问题&#xff0c;但是如果二叉树的节点很多&#xff08;比如 1 亿&#xff…

Oracle——物化视图

文章目录含义物化视图的语法物化视图的创建1、自动刷新的物化事务 ON COMMIT2、非自动刷新的物化视图 ON demand关于手动刷新物化视图的删除资料参考含义 什么是物化视图&#xff1f; 物化视图&#xff0c;通俗点说就是物理化的视图。 什么叫物理化&#xff1f; 将视图以表结构…

2023年全国最新机动车签字授权人精选真题及答案4

百分百题库提供机动车签字授权人考试试题、机动车签字授权人考试预测题、机动车签字授权人考试真题、机动车签字授权人证考试题库等&#xff0c;提供在线做题刷题&#xff0c;在线模拟考试&#xff0c;助你考试轻松过关。 11.使用转化炉原理测量氮氧化物的排气分析仪进行排气污…

python画图中的几个小技巧

主要解决pandas.DataFrame.plot及matlibplot.pyplot画图中&#xff0c;x轴太稠密及其他设置图中各个组件字体大小等问题&#xff0c;以示例展示为主。import pandas as pd import numpy as np import matplotlib.pyplot as plt import matplotlib as mpl pd.set_option(max_col…