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

 更新时间:2017年06月26日 09:08:09   投稿:jingxian  
下面小编就为大家带来一篇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多线程学习之死锁的模拟和避免(实例讲解)就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • Kosaraju算法详解

    Kosaraju算法详解

    这篇文章主要为大家详细介绍了Kosaraju算法,Kosaraju算法可以计算出一个有向图的强连通分量,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-09-09
  • Java面向对象的三大特征

    Java面向对象的三大特征

    这篇文章主要给大家结合相关示例介绍了Java面向对象的三大特征:封装、继承、多态,非常的实用,有需要的小伙伴可以参考下。
    2015-06-06
  • Java各种排序算法汇总(冒泡,选择,归并,希尔及堆排序等)

    Java各种排序算法汇总(冒泡,选择,归并,希尔及堆排序等)

    这篇文章主要介绍了Java各种排序算法,以大量实例形式汇总分析了Java常用的各种排序算法,包括冒泡排序、快速排序、堆排序、插入排序、希尔排序、选择排序、归并排序等,需要的朋友可以参考下
    2015-11-11
  • Java基于字符流形式读写数据的两种实现方法示例

    Java基于字符流形式读写数据的两种实现方法示例

    这篇文章主要介绍了Java基于字符流形式读写数据的两种实现方法示,结合实例形式分析了java逐个字符读写及使用缓冲区进行读写操作的具体实现技巧,需要的朋友可以参考下
    2018-01-01
  • Java使用雪花id生成算法详解

    Java使用雪花id生成算法详解

    SnowFlake算法,是Twitter开源的分布式id生成算法,在2014年开源,开源的版本由scala编写。其核心思想就是-使用一个64bit的long型的数字作为全局唯一id
    2022-12-12
  • 关于mybatis调用存储过程获取返回值问题

    关于mybatis调用存储过程获取返回值问题

    这篇文章主要介绍了mybatis调用存储过程获取返回值问题,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-01-01
  • gRPC实践之proto及Maven插件概念及使用详解

    gRPC实践之proto及Maven插件概念及使用详解

    这篇文章主要为大家介绍了gRPC实践之proto及Maven插件概念及使用详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-04-04
  • Java Bigdecimal使用原理详解

    Java Bigdecimal使用原理详解

    这篇文章主要介绍了Java Bigdecimal使用原理详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-03-03
  • Java使用Collections.sort()排序的示例详解

    Java使用Collections.sort()排序的示例详解

    这篇文章主要介绍了Java使用Collections.sort()排序的示例详解,Collections.sort(list, new PriceComparator());的第二个参数返回一个int型的值,就相当于一个标志,告诉sort方法按什么顺序来对list进行排序。对此感兴趣的可以了解一下
    2020-07-07
  • java多线程:基础详解

    java多线程:基础详解

    这篇文章主要介绍了java多线程编程实例,分享了几则多线程的实例代码,具有一定参考价值,加深多线程编程的理解还是很有帮助的,需要的朋友可以参考下。
    2021-08-08

最新评论