多线程(十一)—— 死锁

死锁

死锁是指两个或者两个以上的线程在执行过程中,由于竞争资源而造成的阻塞问题,若无外力作用下,他们将无法推进下去。此时系统处于死锁状态

死锁产生原因:

1、因竞争资源产生死锁
2、进程顺序推进不当发生死锁

出现死锁的必要条件:

1、互斥条件:资源每次只能是一个线程使用-》资源
2、请求与保持条件:一个线程因请求资源而阻塞时,对以获取的资源保持不放-》线程
3、不可剥夺条件:线程已获取的资源,在未使用之前,不能强行剥夺
4、循环等待条件:若干线程之间形成一种头尾相连接的循环等待资源关系。

死锁的预防或解除

解决死锁的途径:预防、避免、检测和恢复

1、预防死锁(破坏4个必要条件)

a、资源一次性分配(破坏请求与保持条件)
b、可剥夺资源:在线程未满足条件时,释放掉已占有的资源
c、资源有序分配:系统给每类资源赋予一个编号,每个线程按编号递增的顺序请求资源,释放资源顺序相反;

2、避免死锁(银行家算法)

允许线程动态地申请资源。系统在资源分配之前先计算资源分配的安全性,若此次分配不会导致系统进入不安全状态,则给线程分配该资源,否则进程等待。

银行家算法:
共有四种资源,空格隔开;

线程名称已拥有的资源还需要的资源
p00 0 3 20 0 1 2
p11 0 0 01 7 5 0
p21 3 5 42 3 5 6
p30 3 3 20 6 5 2
p40 0 1 40 6 5 6

剩余的总资源为:1 6 2 2

import java.util.ArrayList;
import java.util.Arrays;

/**
 * @类名 BankerAlgorithm
 * @类说明 银行家算法
 * @作者 中都
 * @时间 2019/12/2 20:36
 */
public class BankerAlgorithm {
    private int[][] allocation;  //进程已占有资源
    private int[][] need;   //进程还需要的资源
    private int[] available;  //系统剩余可分配资源
    private int num = 0;     //表示进程数
    private ArrayList<Integer> lastRecord = null;  //记录最终找到的安全序列

    /**
     * 银行家算法
     * @param allocation
     * @param need
     * @param available
     */
    public void bankerAlgorithm(int[][] allocation,int[][] need,int[] available) {
        if(allocation == null || need == null || available == null ||
                allocation.length != need.length){
            System.out.println("参数不合法!");
            return;
        }
        this.allocation = allocation;
        this.need = need;
        this.available = available;
        this.num = allocation.length;
        this.lastRecord = new ArrayList<>();

        //执行安全性算法
        boolean sign = securityAlgorithm(available.clone(),new boolean[num],0,lastRecord);
        if(sign) {
            //找到了安全序列,打印结果
            show();
        }else {
            //没有找到
            System.out.println("当前状态不存在安全序列!");
        }
    }

    /**
     * 安全性算法
     * @param work 当前系统剩余资源
     * @param flag 标记是否已经分配
     * @param count 记录已分配的线程个数
     * @param record 记录安全序列
     * @return
     */
    private boolean securityAlgorithm(int[] work, boolean[] flag, int count, ArrayList<Integer> record) {
        if(count >= num) {
            //已经找到一个安全序列,停止查找
            this.lastRecord = record;
            return true;
        }
        boolean result = false;
        for (int i = 0; i < allocation.length; i++) {
            //如果当前进程未分配资源并且剩余的资源足够给他分配,那么尝试把资源分配该该进程
            if(!flag[i] && adjust(this.need[i],work)) {
                //回收资源
                int[] temp = work.clone();
                recycle(allocation[i],temp);
                //标记当前线程已经分配了资源
                boolean[] sign = flag.clone();
                sign[i] = true;
                ArrayList<Integer> tempRecord = (ArrayList)record.clone();
                tempRecord.add(i);
                result = result || securityAlgorithm(temp,sign,count+1, tempRecord);
            }
        }
        return result;
    }

    /**
     * 回收当前线程占有的资源
     * @param resources
     * @param work
     */
    private void recycle(int[] resources, int[] work) {
        for (int i = 0; i < resources.length; i++) {
            work[i] += resources[i];
        }
    }

    /**
     * 查看剩余的资源是否足够分配给当前进程
     * @param need  进程所需的资源
     * @param residue 系统剩余的资源
     * @return
     */
    private boolean adjust(int[] need,int[] residue) {
        for (int i = 0; i < need.length; i++) {
            if(need[i] > residue[i]) {
                return false; //不满足
            }
        }
        return true;  //满足
    }


    /**
     * 输出结果
     */
    private void show() {
        System.out.println("经过查找,找到一个安全序列,资源分配从上至下顺序如下:");
        System.out.println("初始时刻剩余总资源:"+Arrays.toString(available));
        System.out.println("进程名    拥有资源         需要资源         系统剩余资源      分配后回收系统剩余资源");
        for (int i = 0; i < this.lastRecord.size(); i++) {
            int index = lastRecord.get(i);
            System.out.print(" p"+index);
            System.out.print("     "+Arrays.toString(allocation[index]));
            System.out.print("     "+Arrays.toString(need[index]));
            System.out.print("     "+Arrays.toString(available));
            recycle(allocation[index],available);
            System.out.println("        "+Arrays.toString(available));
        }
    }
}

测试类:

public class Test {
    public static void main(String[] args) {
        int[] l1 = {0,0,3,2};
        int[] l2 = {1,0,0,0};
        int[] l3 = {1,3,5,4};
        int[] l4 = {0,3,3,2};
        int[] l5 = {0,0,1,4};
        int[][] alreadyOwn = {l1,l2,l3,l4,l5};  //已拥有
        int[] m1 = {0,0,1,2};
        int[] m2 = {1,7,5,0};
        int[] m3 = {2,3,5,6};
        int[] m4 = {0,6,5,2};
        int[] m5 = {0,6,5,6};
        int[][] need = {m1,m2,m3,m4,m5};   //需要
        int[] residue = {1,6,2,2};   //剩余
        new BankerAlgorithm().bankerAlgorithm(alreadyOwn,need,residue);
    }
}

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

3、检测与接触死锁

当线程发现进入死锁了,立即从死锁状态解除掉,采用方式剥夺资源:从其他线程剥夺足够多的资源给死锁线程,以免除死锁状态的线程;

4、如何检查程序中的死锁?

实际定位死锁问题的思路:

  1. 首先需要确定java进程是否发生死锁
  2. 使用在Java JDK目录bin目录下的工具jvisualvm或命令jstack;
  3. ①打开jvisualvm工具,专门分析JVM CPU,内存使用情况,以及线程的运行信息查看当前java进程各个线程运行的状态(颜色);② 通过jvisualvm的线程dump或者jstack命令,把当前java进程所有线程的调用堆栈信息打印出来;/或者使用jstack命令,下面的步骤都是一样的;
  4. 分析main线程和子线程有没有关键短语: waiting for(资源地址) waiting to lock(资源地址)
  5. 看线程函数调用栈,定位到源码上,具体问题具体分析;

在这里插入图片描述
下面是一个会发生死锁的代码:

package dui;

/**
 * @ClassName Test4
 * @Description 实验死锁的定位
 * @Author lzq
 * @Date 2019/8/3 11:36
 * @Version 1.0
 **/
public class Test4 {
    private static Object objA = new Object();
    private static Object objB = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(new Thread1());
        Thread thread2 = new Thread(new Thread2());
        thread1.start();
        thread2.start();
    }

    private static class Thread1 implements Runnable{

        @Override
        public void run() {
            synchronized (objA) {
                System.out.println("线程1得到A对象的锁");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (objB) {
                    System.out.println("线程1得到B对象的锁");
                }
            }
        }
    }

    private static class Thread2 implements Runnable{

        @Override
        public void run() {
            synchronized (objB) {
                System.out.println("线程2得到B对象的锁");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (objA) {
                    System.out.println("线程2得到A对象的锁");
                }
            }
        }
    }

}
定位死锁
1、使用jvisualvm工具

打开jvisualvm工具,运行上面程序:
在这里插入图片描述
点击Dump:
在这里插入图片描述
可以定位到,在33、51行有问题;

2、使用jstack命令

先打开命令行窗口,到jdk的bin目录下,运行程序,输入jps命令,查看正在运行的进程号:
在这里插入图片描述
输入jstack+进程号:
在这里插入图片描述
往下找:
在这里插入图片描述

引起死锁的案例:
1、生产者、消费者使用不当会产生死锁(wait、notify、notifyAll)
2、多线程获取多把锁

lock1  lock2

Lock1.lock();
  lock2.lock();
  //todo
  lock2.unlock();
lock1.unlock();

3、哲学家就餐问题

  • 圆桌上有5位哲学家、每两位中间有一个筷子
  • 每个哲学家有两件事要做 思考、吃饭(哲学家必须同时拿到两个筷子才能吃饭)
  • 哲学家之间并不知道对方何时要吃饭、何时要思考,不能协商制定吃饭、思考策略
  • 制定一个拿筷子的策略,使得哲学家不会因为拿筷子而出现死锁乐观锁

哲学家就餐问题:
这个问题我的解决办法是加入一个服务员类,由服务员发放和回收筷子,用以解决死锁问题:

/**
 * @ClassName Philosopher
 * @Description 哲学家
 * @Author lzq
 * @Date 2019/4/29 0:02
 * @Version 1.0
 **/
public class Philosopher extends Thread{
    private Waiter waiter = null;

    public Philosopher(Waiter waiter){
        this.waiter = waiter;
    }

    @Override
    public void run() {
        while (true) {
            waiter.allocation();
        }
    }
}
/**
 * @ClassName Chopsticks
 * @Description 筷子
 * @Author lzq
 * @Date 2019/4/28 23:00
 * @Version 1.0
 **/
public class Chopsticks {
    private int number = 0; //筷子的编号

    public Chopsticks(int number) {
        this.number = number;
    }

    public int getNumber() {
        return number;
    }
}
import java.util.Random;

/**
 * @ClassName Waiter
 * @Description 服务员
 * @Author lzq
 * @Date 2019/4/28 23:14
 * @Version 1.0
 **/
public class Waiter {
    //存放5只筷子的数组
    private Chopsticks[] chopsticks = new Chopsticks[5];

    public Waiter(Chopsticks chopsticks1, Chopsticks chopsticks2, Chopsticks chopsticks3,
                  Chopsticks chopsticks4, Chopsticks chopsticks5) {
        chopsticks[0] = chopsticks1;
        chopsticks[1] = chopsticks2;
        chopsticks[2] = chopsticks3;
        chopsticks[3] = chopsticks4;
        chopsticks[4] = chopsticks5;
    }

    Random random = new Random();

    /**
     * 分配筷子
     */
    public synchronized void allocation() {
        try {
             int x1 = random.nextInt(5);
             int x2 = random.nextInt(5);
             while (x1 == x2) {
                 x2 = random.nextInt(5);
             }
             Chopsticks chop1 = chopsticks[x1];
             Chopsticks chop2 = chopsticks[x2];
             String name = Thread.currentThread().getName();
             System.out.println(name+"拿到"+chop1.getNumber()+"号筷子、"+chop2.getNumber()+"号筷子,开始就餐......");
             Thread.sleep(500);
             System.out.println(name+"就餐完毕,放下筷子,开始思考......");
             System.out.println("---------------------------------");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
/**
 * @ClassName TestDemo25
 * @Description 哲学家就餐问题
 * @Author lzq
 * @Date 2019/4/28 22:58
 * @Version 1.0
 **/
public class TestDemo25 {
    public static void main(String[] args) {
        //5只筷子
        Chopsticks chopsticks1 = new Chopsticks(1);
        Chopsticks chopsticks2 = new Chopsticks(2);
        Chopsticks chopsticks3 = new Chopsticks(3);
        Chopsticks chopsticks4 = new Chopsticks(4);
        Chopsticks chopsticks5 = new Chopsticks(5);
        Waiter waiter = new Waiter(chopsticks1,chopsticks2,chopsticks3,chopsticks4,chopsticks5);
        Thread t1 = new Philosopher(waiter);
        Thread t2 = new Philosopher(waiter);
        Thread t3 = new Philosopher(waiter);
        Thread t4 = new Philosopher(waiter);
        Thread t5 = new Philosopher(waiter);
        t1.setName("哲学家1");
        t2.setName("哲学家2");
        t3.setName("哲学家3");
        t4.setName("哲学家4");
        t5.setName("哲学家5");
        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
    }
}

在这里插入图片描述
这个解法我觉得不好,虽然保证了并发问题,但是有点不合实际,5筷子、5个哲学家,同时是可以有两个人吃饭的,我这个每次只能有一个人吃饭,还有其他的解决办法,我觉得那个哲学家在拿筷子的时候,确保左右都有筷子才同时拿起左右两只筷子比较好:

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

/**
 * @ClassName Chopsticks
 * @Description 筷子
 * @Author lzq
 * @Date 2019/8/4 19:45
 * @Version 1.0
 **/
public class Chopsticks {
    private int count = 5;
    private List<Boolean> chops = new ArrayList<>();

    public Chopsticks(int count) {
        this.count = count;
        for (int i = 1; i <= this.count; i++) {
            chops.add(false);
        }
    }
    /**
     * 获取筷子
     */
    public synchronized void getChop() {
        String threadName = Thread.currentThread().getName();
        int index = Integer.valueOf(threadName);
        int left = index % 5;
        int right = (index + 1) % 5;
        while (chops.get(left) || chops.get(right)) {
            //拿左右两边的筷子   进来就意味着至少有一只筷子被别人占用
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //出了上面的循环,就意味着当前哲学家可以吃饭了,占用筷子
        chops.set(left, true);
        chops.set(right, true);

        System.out.println(threadName + " 哲学家拿到 " + left + "号筷子和" + right + "号筷子 开始就餐...");
    }

    /**
     * 放下筷子,两个操作写开,就可以同时两个人用四个筷子吃饭了
     */
    public synchronized void freeChop() {
        String threadName = Thread.currentThread().getName();
        int index = Integer.valueOf(threadName);
        int left = index % 5;
        int right = (index + 1) % 5;
        System.out.println(threadName + " 哲学家放下 " + left + "号筷子和" + right + "号筷子 开始思考...");

        chops.set(left, false);
        chops.set(right, false);
        notifyAll();
    }
}
/**
 * @ClassName Philosopher
 * @Description 哲学家
 * @Author lzq
 * @Date 2019/8/4 20:08
 * @Version 1.0
 **/
public class Philosopher extends Thread{
    private Chopsticks chopsticks;

    public Philosopher(Chopsticks chopsticks) {
        this.chopsticks = chopsticks;
    }

    @Override
    public void run() {
        while (true) {
            this.chopsticks.getChop();
            try {
                Thread.sleep(500);   //模拟吃饭时间
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.chopsticks.freeChop();
        }
    }
}

/**
 * @ClassName Test
 * @Description 哲学家就餐
 * @Author lzq
 * @Date 2019/8/4 20:10
 * @Version 1.0
 **/
public class Test {
    public static void main(String[] args) {
        Chopsticks chopsticks = new Chopsticks(5);
        Philosopher philosopher1 = new Philosopher(chopsticks);
        Philosopher philosopher2 = new Philosopher(chopsticks);
        Philosopher philosopher3 = new Philosopher(chopsticks);
        Philosopher philosopher4 = new Philosopher(chopsticks);
        Philosopher philosopher5 = new Philosopher(chopsticks);

        philosopher1.setName("1");
        philosopher2.setName("2");
        philosopher3.setName("3");
        philosopher4.setName("4");
        philosopher5.setName("5");

        philosopher1.start();
        philosopher2.start();
        philosopher3.start();
        philosopher4.start();
        philosopher5.start();
    }
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值