java多线程学习之死锁的模拟和避免(实例讲解)

1.死锁

死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

Java 死锁产生的四个必要条件:

1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用

2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。

3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。

4、循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。

当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。

2.模拟一个死锁

package com.tl.skyLine.thread; 

import java.util.Date; 

/**
 * Created by tl on 17/3/3.
 */
public class DeadLock {
 public static String bowl = "碗";
 public static String chopsticks = "筷子"; 

 public static void main(String[] args) {
  LockA la = new LockA();
  new Thread(la).start();
  LockB lb = new LockB();
  new Thread(lb).start();
 } 

} 

class LockA implements Runnable {
 public void run() {
  try {
   System.out.println(new Date().toString() + "邹保健开始拿餐具吃饭");
   while (true) {
    synchronized (DeadLock.bowl) {
     System.out.println(new Date().toString() + "邹保健抢到了碗");
     Thread.sleep(3000); // 此处等待是给B能锁住机会
     synchronized (DeadLock.chopsticks) {
      System.out.println(new Date().toString() + "邹保健抢到了筷子");
      Thread.sleep(60 * 1000); // 为测试,占用了就不放
     }
    }
   }
  } catch (Exception e) {
   e.printStackTrace();
  }
 }
} 

class LockB implements Runnable {
 public void run() {
  try {
   System.out.println(new Date().toString() + "陈顶天开始拿餐具吃饭");
   while (true) {
    synchronized (DeadLock.chopsticks) {
     System.out.println(new Date().toString() + "陈顶天抢到了筷子");
     Thread.sleep(3000); // 此处等待是给A能锁住机会
     synchronized (DeadLock.bowl) {
      System.out.println(new Date().toString() + "陈顶天抢到了碗");
      Thread.sleep(60 * 1000); // 为测试,占用了就不放
     }
    }
   }
  } catch (Exception e) {
   e.printStackTrace();
  }
 }
} 

结果:

Fri Mar 03 16:34:36 CST 2017陈顶天开始拿餐具吃饭
Fri Mar 03 16:34:37 CST 2017陈顶天抢到了筷子
Fri Mar 03 16:34:36 CST 2017邹保健开始拿餐具吃饭
Fri Mar 03 16:34:37 CST 2017邹保健抢到了碗 

结果陈顶天同学抢到了参筷子,拿着不放,邹保健同学抢到了碗,也死活不放手,但是只有一双筷子和一双碗,结果就是双双饿死。。。

3.避免死锁

假如我们是陈顶天和邹保健同学的同事,肯定不忍心看到他们饿死,那么怎么办呢?

我们就要采取方法避免思索的发生,这边介绍两种方法,一种是加锁顺序(线程按照一定的顺序加锁);另一种是加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁);

3.1 加锁顺序

当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。我们上面的代码为了模拟死锁,将线程LockA与LockB两位同事的抢夺资源顺序做了一个调整,LockA先抢碗,然后sleep3秒,LockB先抢筷子,我们现在把争夺资源顺序改一下,两个线程都是先抢碗,再抢筷子,严格按照这个顺序运行,那么A抢到碗以后,B去争夺资源,必须从抢碗开始,不能在抢不到碗的情况下去抢筷子,这样就避免死锁的发生,这也是避免死锁最简单的方法。

代码修改如下:

/**
 * Created by tl on 17/3/3.
 */
public class UnDeadLock {
 public static String bowl = "碗";
 public static String chopsticks = "筷子"; 

 public static void main(String[] args) {
  LockA la = new LockA();
  new Thread(la).start();
  LockB lb = new LockB();
  new Thread(lb).start();
 } 

} 

class LockA implements Runnable {
 public void run() {
  try {
   System.out.println(new Date().toString() + "邹保健开始拿餐具吃饭");
   while (true) {
    synchronized (UnDeadLock.bowl) {
     System.out.println(new Date().toString() + "邹保健抢到了碗");
     synchronized (UnDeadLock.chopsticks) {
      System.out.println(new Date().toString() + "邹保健抢到了筷子");
     }
    }
    Thread.sleep(5000);
   }
  } catch (Exception e) {
   e.printStackTrace();
  }
 }
} 

class LockB implements Runnable {
 public void run() {
  try {
   System.out.println(new Date().toString() + "陈顶天开始拿餐具吃饭");
   while (true) {
    synchronized (UnDeadLock.bowl) {
     System.out.println(new Date().toString() + "陈顶天抢到了碗");
     synchronized (UnDeadLock.chopsticks) {
      System.out.println(new Date().toString() + "陈顶天抢到了筷子");
     }
    }
    Thread.sleep(5000);
   }
  } catch (Exception e) {
   e.printStackTrace();
  }
 }
} 

此时运行结果

Fri Mar 24 11:16:51 CST 2017邹保健开始拿餐具吃饭
Fri Mar 24 11:16:51 CST 2017陈顶天开始拿餐具吃饭
Fri Mar 24 11:16:51 CST 2017邹保健抢到了碗
Fri Mar 24 11:16:51 CST 2017邹保健抢到了筷子
Fri Mar 24 11:16:51 CST 2017陈顶天抢到了碗
Fri Mar 24 11:16:51 CST 2017陈顶天抢到了筷子
Fri Mar 24 11:16:56 CST 2017邹保健抢到了碗
Fri Mar 24 11:16:56 CST 2017邹保健抢到了筷子
Fri Mar 24 11:16:56 CST 2017陈顶天抢到了碗
Fri Mar 24 11:16:56 CST 2017陈顶天抢到了筷子
Fri Mar 24 11:17:01 CST 2017邹保健抢到了碗
Fri Mar 24 11:17:01 CST 2017邹保健抢到了筷子 

就不会再出现死锁的情况了。

3.2 加锁时效

加锁时效的原理就是:给每一个访问线程增加访问时效,若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁(此时就打破了造成死锁的四个原因中的第三个原因),然后等待一段随机的时间再重试。

为了实现这个目标,我们不使用显示的去锁,我而是用信号量Semaphore去控制。信号量可以控制资源能被多少线程访问,这里我们指定只能被一个线程访问,就做到了类似锁住。而信号量可以指定去获取的超时时间,我们可以根据这个超时时间,去做一个额外处理。对于无法成功获取的情况,一般就是重复尝试,或指定尝试的次数,也可以马上退出。

package com.tl.skyLine.thread; 

import java.util.Date;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit; 

/**
 * Created by tl on 17/3/3.
 */
public class UnDeadLock {
 public static String bowl = "碗";
 //信号量可以碗只能能被一个线程同时访问
 public static final Semaphore a1 = new Semaphore(1);
 public static String chopsticks = "筷子";
 //信号量可以筷子只能能被一个线程同时访问
 public static final Semaphore a2 = new Semaphore(1); 

 public static void main(String[] args) {
  LockAa la = new LockAa();
  new Thread(la).start();
  LockBa lb = new LockBa();
  new Thread(lb).start();
 } 

} 

class LockAa implements Runnable {
 public void run() {
  try {
   System.out.println(new Date().toString() + "邹保健开始拿餐具吃饭");
   while (true) {
    if (UnDeadLock.a1.tryAcquire(1, TimeUnit.SECONDS)) {
     System.out.println(new Date().toString() + "邹保健抢到了碗");
     if (UnDeadLock.a2.tryAcquire(1, TimeUnit.SECONDS)) {
      System.out.println(new Date().toString() + "邹保健抢到了筷子,凑齐了餐具,准备吃饭");
      Thread.sleep(60 * 1000 * 10); // 抢到餐具就开始吃饭,吃饭时间十分钟
     } else {
      System.out.println(new Date().toString() + "筷子已经被抢走了,邹保健抢筷子失败");
     }
    } else {
     System.out.println(new Date().toString() + "碗已经被抢走了,邹保健抢碗失败");
    } 

    UnDeadLock.a1.release(); // 释放
    UnDeadLock.a2.release();
    System.out.println(new Date().toString() + "邹保健把抢到的部分餐具又放回原处");
    Thread.sleep(1000); // 马上进行尝试,现实情况下do something是不确定的
   }
  } catch (Exception e) {
   e.printStackTrace();
  }
 }
} 

class LockBa implements Runnable {
 public void run() {
  try {
   System.out.println(new Date().toString() + "陈顶天开始拿餐具吃饭");
   while (true) {
    if (UnDeadLock.a2.tryAcquire(1, TimeUnit.SECONDS)) {
     System.out.println(new Date().toString() + "陈顶天抢到了筷子");
     if (UnDeadLock.a1.tryAcquire(1, TimeUnit.SECONDS)) {
      System.out.println(new Date().toString() + "陈顶天抢到了碗,凑齐了餐具,准备吃饭");
      Thread.sleep(60 * 1000 * 10); // 抢到餐具就开始吃饭,吃饭时间十分钟
     } else {
      System.out.println(new Date().toString() + "碗已经被抢走了,陈顶天抢碗失败");
     }
    } else {
     System.out.println(new Date().toString() + "筷子已经被抢走了,陈顶天抢筷子失败");
    } 

    UnDeadLock.a1.release(); // 释放
    UnDeadLock.a2.release();
    System.out.println(new Date().toString() + "陈顶天把抢到的部分餐具又放回原处");
    Thread.sleep(10 * 1000);//这里只是为了演示,所以tryAcquire只用1秒,而且B要给A让出能执行的时间,否则两个永远是死锁
   }
  } catch (Exception e) {
   e.printStackTrace();
  }
 }
} 

结果

Fri Mar 03 18:12:07 CST 2017邹保健开始拿餐具吃饭
Fri Mar 03 18:12:07 CST 2017陈顶天开始拿餐具吃饭
Fri Mar 03 18:12:07 CST 2017邹保健抢到了碗
Fri Mar 03 18:12:07 CST 2017陈顶天抢到了筷子
Fri Mar 03 18:12:08 CST 2017筷子已经被抢走了,邹保健抢筷子失败
Fri Mar 03 18:12:08 CST 2017邹保健把抢到的部分餐具又放回原处
Fri Mar 03 18:12:08 CST 2017陈顶天抢到了碗,凑齐了餐具,准备吃饭
Fri Mar 03 18:12:10 CST 2017碗已经被抢走了,邹保健抢碗失败
Fri Mar 03 18:12:10 CST 2017邹保健把抢到的部分餐具又放回原处
Fri Mar 03 18:12:11 CST 2017邹保健抢到了碗
Fri Mar 03 18:12:11 CST 2017邹保健抢到了筷子,凑齐了餐具,准备吃饭 

很明显看到,我们打破了满足死锁的第三条,即当资源请求者在请求其他的资源的同时保持对原有资源的占有,当没有完全抢到碗和筷子的时候,两个人(线程)全部释放占有的资源,重新开始争抢资源,这样一个人抢到碗和筷子之后,吃饱再给另一个吃,这样你就成功了拯救了你的两位同事!

Semaphore api:

acquire 

public void acquire()
    throws InterruptedException 

 从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断。获取一个许可(如果提供了一个)并立即返回,将可用的许可数减 1。 

 如果没有可用的许可,则在发生以下两种情况之一前,禁止将当前线程用于线程安排目的并使其处于休眠状态: 

  某些其他线程调用此信号量的 release() 方法,并且当前线程是下一个要被分配许可的线程;或者
  其他某些线程中断当前线程。 

 如果当前线程: 

  被此方法将其已中断状态设置为 on ;或者
  在等待许可时被中断。 

 则抛出 InterruptedException,并且清除当前线程的已中断状态。 

 抛出:
  InterruptedException - 如果当前线程被中断 

release 

public void release() 

 释放一个许可,将其返回给信号量。释放一个许可,将可用的许可数增加 1。如果任意线程试图获取许可,则选中一个线程并将刚刚释放的许可给予它。然后针对线程安排目的启用(或再启用)该线程。 

 不要求释放许可的线程必须通过调用 acquire() 来获取许可。通过应用程序中的编程约定来建立信号量的正确用法。

wait()与sleep()的区别:

sleep()方法只让出了CPU,而并不会释放同步资源锁!!!

wait()方法则是指当前线程让自己暂时退让出同步资源锁,以便其他正在等待该资源的线程得到该资源进而运行;

以上这篇java多线程学习之死锁的模拟和避免(实例讲解)就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • Java多线程之死锁的出现和解决方法

    什么是死锁? 死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放.由于线程被无限期地阻塞,因此程序不能正常运行.形象的说就是:一个宝藏需要两把钥匙来打开,同时间正好来了两个人,他们一人一把钥匙,但是双方都再等着对方能交出钥匙来打开宝藏,谁都没释放自己的那把钥匙.就这样这俩人一直僵持下去,直到开发人员发现这个局面. 导致死锁的根源在于不适当地运用"synchronized"关键词来管理线程对特定对象的访问."synchronized"关

  • Java多线程 线程同步与死锁

     Java多线程 线程同步与死锁 1.线程同步 多线程引发的安全问题 一个非常经典的案例,银行取钱的问题.假如你有一张银行卡,里面有5000块钱,然后你去银行取款2000块钱.正在你取钱的时候,取款机正要从你的5000余额中减去2000的时候,你的老婆正巧也在用银行卡对应的存折取钱,由于取款机还没有把你的2000块钱扣除,银行查到存折里的余额还剩5000块钱,准备减去2000.这时,有趣的事情发生了,你和你的老婆从同一个账户共取走了4000元,但是账户最后还剩下3000元. 使用代码模拟下取款过

  • java 多线程死锁详解及简单实例

    java 多线程死锁 相信有过多线程编程经验的朋友,都吃过死锁的苦.除非你不使用多线程,否则死锁的可能性会一直存在.为什么会出现死锁呢?我想原因主要有下面几个方面: (1)个人使用锁的经验差异     (2)模块使用锁的差异     (3)版本之间的差异     (4)分支之间的差异     (5)修改代码和重构代码带来的差异 不管什么原因,死锁的危机都是存在的.那么,通常出现的死锁都有哪些呢?我们可以一个一个看过来,     (1)忘记释放锁 void data_process() { Ent

  • java多线程学习之死锁的模拟和避免(实例讲解)

    1.死锁 死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放.由于线程被无限期地阻塞,因此程序不可能正常终止. Java 死锁产生的四个必要条件: 1.互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用 2.不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放. 3.请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有. 4.循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的

  • Java多线程环境下死锁模拟

    目录 1.死锁产生的条件 2.模拟多线程环境下死锁的产生 3.死锁的排查 1.死锁产生的条件 互斥:一次只有一个进程可以使用一个资源.其他进程不能访问已分配给其他进程的资源. 不可抢占:不能抢占进程已占有的资源 请求和保持:当一个进程等待其他进程释放资源时,继续占有已经分配的资源 循环等待:存在一个封闭的进程链,使得每个进程至少占有此链中下一个进程所需要的一个资源. 注意:前三个条件都只是死锁存在的必要条件,但不是充分条件.第四个条件是充分条件.以上条件同样适用于线程. 2.模拟多线程环境下死锁

  • Java多线程学习笔记

    目录 多任务.多线程 程序.进程.线程 学着看jdk文档 线程的创建 1.继承Thread类 2.实现Runable接口 理解并发的场景 龟兔赛跑场景 实现callable接口 理解函数式接口 理解线程的状态 线程停止 线程休眠sleep 1.网路延迟 2.倒计时等 线程礼让yield 线程强制执行 观察线程状态 线程的优先级 守护线程 线程同步机制 1.synchronized 同步方法 2.同步块synchronized(Obj){} lock synchronized与lock 多任务.多

  • 【Java IO流】字节流和字符流的实例讲解

    字节流和字符流 对于文件必然有读和写的操作,读和写就对应了输入和输出流,流又分成字节和字符流. 1.从对文件的操作来讲,有读和写的操作--也就是输入和输出. 2.从流的流向来讲,有输入和输出之分. 3.从流的内容来讲,有字节和字符之分. 这篇文章先后讲解IO流中的字节流和字符流的输入和输出操作. 一.字节流 1)输入和输出流 首先,字节流要进行读和写,也就是输入和输出,所以它有两个抽象的父类InputStream.OutputStream. InputStream抽象了应用程序读取数据的方式,即

  • Python 模拟购物车的实例讲解

    1.功能简介 此程序模拟用户登陆商城后购买商品操作.可实现用户登陆.商品购买.历史消费记查询.余额和消费信息更新等功能.首次登陆输入初始账户资金,后续登陆则从文件获取上次消费后的余额,每次购买商品后会扣除相应金额并更新余额信息,退出时也会将余额和消费记录更新到文件以备后续查询. 2.实现方法 架构: 本程序采用python语言编写,将各项任务进行分解并定义对应的函数来处理,从而使程序结构清晰明了.主要编写了六个函数: (1)login(name,password) 用户登陆函数,实现用户名和密码

  • python3 模拟登录v2ex实例讲解

    闲的无聊... 网上一堆,正好练手(主要是新手) # coding=utf-8 import requests from bs4 import BeautifulSoup headers = { 'user-agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36', 'origin': 'https://ww

  • Java 普通代码块静态代码块执行顺序(实例讲解)

    如下所示: class B { public B() { super(); System.out.println("构造器B"); } { System.out.println("普通的代码块B"); } static{ System.out.println("静态代码块B"); } } public class ClassA extends B { public ClassA() { super(); System.out.println(&q

  • Vuejs2 + Webpack框架里,模拟下载的实例讲解

    在实际的开发工作中,难免要配合销售人员,提前做一些前端的 DEMO 出来.这个时候往往还没有连接后端 API.假如要演示一个下载连接,那么应该如何做呢? 我们希望能够达成以下两点: 1.在开发环境下,我们可以在 webpack-dev-server 开发服务器上点击下载连接,点击后浏览器就能不下载文件. 2.当演示的时候,代码编译后放到 nginx 中.用户可以点击下载链接.nginx存放的都是业务代码. 那么如何做到这两点呢?假如我们要模拟下载一个 test.docx 文件.我们可以利用 ur

  • java接入创蓝253短信验证码的实例讲解

    说明 项目是springboot框架 1.短信配置文件 包含验证码发送路径.用户名.密码 chuanglan.requesturl= chuanglan.account= chuanglan.pswd= 配置文件 具体值 查看官网 位置查看截图 红框已经标红 2.读取配置文件类 3.发送数据request实体类 public class SmsVariableRequest { private String account; private String password; private St

  • Java 多线程学习详细总结

    目录(?)[-] 一扩展javalangThread类 二实现javalangRunnable接口 三Thread和Runnable的区别 四线程状态转换 五线程调度 六常用函数说明 使用方式 为什么要用join方法 七常见线程名词解释 八线程同步 九线程数据传递 本文主要讲了java中多线程的使用方法.线程同步.线程数据传递.线程状态及相应的一些线程函数用法.概述等. 首先讲一下进程和线程的区别: 进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1

随机推荐