10.5 线程死锁和协作

  多线程同步,解决的是多线程安全性的问题,避免获取错误的数据,但同步也同时会带来性能损耗和线程死锁的问题。本节通过案例演示什么是线程死锁,并简单介绍解决线程死锁的方法。解决了多线程之间的问题后,本节还会介绍线程之间相互协作,通过多线程间的协作完成系统的功能。

10.5.1 线程死锁

  多线程同步的好处是避免了线程获取错误数据,但多线程同步也带来了性能问题。多线程同步采用了同步代码块和同步方法的方式,依靠的是锁机制实现了互斥访问。因为是互斥的访问,所以不能并行处理,存在性能问题。

  多线程同步的性能问题还只是快和慢的问题,但如果出现了线程死锁,那可能直接导致程序众多的线程都处于阻塞状态,无法继续运行。

  如果线程A只有等待另一个线程B的完成才能继续,而在线程B中又要等待线程A的资源,那么这两个线程相互等待对方释放锁时就会发生死锁。出现死锁后,不会出现异常,不会出现提示,只是相关线程都处于阻塞状态,无法继续运行。

  下面仍然通过一个案例来演示线程的死锁,具体代码如下:

  1. public class DeadLockThread{
  2. //创建两个线程之间竞争使用的对象
  3. private static Object lock1 = new Object();
  4. private static Object lock2 = new Object();
  5. public static void main(String[] args){
  6. new Thread(new ShareThread1()).start();
  7. new Thread(new ShareThread2()).start();
  8. }
  9. private static class ShareThread1 implements Runnable
  10. {
  11. public void run(){
  12. synchronized(lock1){
  13. try{
  14. Thread.sleep(50);
  15. }catch(InterruptedException e)
  16. {
  17. e.printStackTrace();
  18. }
  19. synchronized(lock2){
  20. System.out.println("ShareThread1");
  21. }
  22. }
  23. }
  24. }
  25. private static class ShareThread2 implements Runnable
  26. {
  27. public void run(){
  28. synchronized(lock2){
  29. try{
  30. Thread.sleep(50);
  31. }catch(InterruptedException e)
  32. {
  33. e.printStackTrace();
  34. }
  35. synchronized(lock1){
  36. System.out.println("ShareThread2");
  37. }
  38. }
  39. }
  40. }
  41. }

  上面的代码中,创建了两个线程之间竞争使用的对象lock1和lock2,内部类ShareThread1在run()方法中先对lock1上锁,然后对lock2上锁,并且只有lock2代码块运行结束解锁之后,lock1才能运行结束解锁。类似的内部类ShareThread2在run()方法中先对lock2上锁,然后对lock1上锁,并且只有lock1代码块运行结束解锁之后,lock2才能运行结束解锁。当这两个线程启动以后,分别都握着第一个锁,等待第二个锁,程序死锁!

  当多个线程竞争多个排他性锁的时候,可能出现死锁。解决的方式为多个线程以同样的顺序获取锁,不出现交叉也就不会出现死锁的问题。

10.5.2 产生死锁的原因及条件

  为什么会产生死锁?什么情况下可能会导致死锁?下面,我们就一起来探讨死锁产生的原因及必要条件。

  死锁产生的原因有以下三个方面。

  (1)系统资源不足。如果系统的资源充足,所有进程的资源请求都能够得到满足,自然就不会发生死锁。

  (2)进程运行推进的顺序不合适。

  (3)资源分配不当等。

  产生死锁的必要条件有以下四个。

  (1)互斥条件:一个资源每次只能被一个进程使用。

  (2)请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

  (3)不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。

  (4)循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

  只要系统发生死锁,这四个条件就必然成立;反之,只要破坏四个条件中的任意一个,就可以避免死锁的产生。

10.5.3 线程协作

  通过之前的学习,已经了解并初步解决了多线程之间可能出现的问题,下一步学习的重点是如何让线程之间进行有效协作。线程协作的一个典型案例就是生产者和消费者问题,生产者和消费者的这种协作是通过线程之间的握手来实现的,而这种握手又是通过Object类的wait()和notify()方法来实现的。下面具体来了解生产者和消费者问题。

  有一家餐厅举办吃热狗活动,活动时有5个顾客来吃,3个厨师来做。为了避免浪费,制作好的热狗被放进一个能装10个热狗的长条状容器中,并且按照先进先出的原则取热狗。如果长条容器被装满,则厨师已经做完的热狗不再往长条容器里放,同时停止做热狗;如果顾客发现长条容器内的热狗吃完了,则提醒厨师再做热狗。这里的厨师就是生产者,顾客就是消费者。

  这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件。对于生产者,当生产的产品装满了仓库,则需要停止生产,等待消费者消费后提醒生产者继续生产。对于消费者,当发现仓库中已没有产品时,则不能消费,等待生产者生产出产品以后通知消费者可以消费。

  之前学习的synchronized关键字可实现对共享资源的互斥操作,但无法实现不同线程之间消息的传递。Java提供了wait()、notify()、notifyAll()三个方法,解决线程之间协作的问题。这三个方法均是java.lang.Object类的方法,但都只能在同步方法或者同步代码块中使用,否则会抛出异常。下面是这三个方法的简单介绍。

  • void wait()

  当前线程等待,等待其他线程调用此对象的notify()方法或notifyAll()方法将其唤醒。

  • void notify()

  唤醒在此对象锁上等待的单个线程。

  • void notifyAll()

  唤醒在此对象锁上等待的所有线程。

  图10.10所示的是线程等待与唤醒的示意图。

  完成吃热狗活动的需求有一定的难度,现整理思路如下。

  (1)定义一个集合模拟长条容器存放热狗,集合里实际存放Integer对象,其数值代表热狗的编号(热狗编号规则举例:300002代表编号为3的厨师做的第2个热狗),这样能通过集合添加和删除操作实现长条容器内热狗的先进先出。

  (2)以热狗集合作为对象锁,所有对热狗集合的操作(在长条容器中添加或取走热狗)互斥,这样保证不会出现多个顾客同时取最后剩下的一个热狗的情况,也不会出现多个厨师同时添加热狗造成长条容器里热狗数大于10个的情况。

10.5 线程死锁和协作 - 图1


图10.10 线程等待与唤醒

  (3)当厨师希望往长条容器中添加热狗时,如果发现长条容器中已有10个热狗,则停止做热狗,等待顾客从长条容器中取走热狗的事件发生,以唤醒厨师可以重新进行判断,是否需要做热狗。

  (4)当顾客希望从长条容器中取走热狗时,如果发现长条容器中已没有热狗,则停止吃热狗,等待厨师往长条容器中添加热狗的事件发生,以唤醒顾客可以重新进行判断,是否可以取走热狗吃。

  实现此功能的代码如下:

  1. import java.util.*;
  2. public class TestProdCons {
  3. //定义一个存放热狗的集合,里面存放的是整数,代表热狗编号
  4. private static final List<Integer> hotDogs = new ArrayList<Integer>();
  5. public static void main(String[] args){
  6. for(int i = 1;i <= 3;i++){
  7. new Producer(i).start();
  8. }
  9. for(int i = 1;i <= 5;i++){
  10. new Consumer(i).start();
  11. }
  12. try{
  13. Thread.sleep(2000);
  14. }catch(InterruptedException e){
  15. e.printStackTrace();
  16. }
  17. System.exit(0);
  18. }
  19. //生产者线程,以热狗集合作为对象锁,所有对热狗集合的操作互斥
  20. private static class Producer extends Thread{
  21. int i = 1;
  22. int pid = -1;
  23. public Producer(int id){
  24. this.pid = id;
  25. }
  26. public void run(){
  27. while(true){
  28. try{
  29. //模拟消耗的时间
  30. Thread.sleep(100);
  31. } catch (InterruptedException e) {
  32. e.printStackTrace();
  33. }
  34. synchronized(hotDogs){
  35. if(hotDogs.size() < 10){
  36. //热狗编号,300002代表编号为3的生产者生产的第2个热狗
  37. hotDogs.add(pid*10000 + i);
  38. System.out.println("生产者" + pid + "生产热狗,编号为:" + pid*10000 + i);
  39. i++;
  40. //唤醒hotDogs对象锁上所有调用wait()方法的线程
  41. hotDogs.notifyAll();
  42. }else{
  43. try{
  44. System.out.println("热狗数已到10个,等待消费!");
  45. hotDogs.wait();
  46. }catch(InterruptedException e) {
  47. e.printStackTrace();
  48. }
  49. }
  50. }
  51. }
  52. }
  53. }
  54. //消费者线程,以热狗集合作为对象锁,所有对热狗集合的操作互斥
  55. private static class Consumer extends Thread {
  56. int cid = -1;
  57. public Consumer(int id){
  58. this.cid = id;
  59. }
  60. public void run(){
  61. while(true){
  62. synchronized (hotDogs) {
  63. try{
  64. //模拟消耗的时间
  65. Thread.sleep(200);
  66. }catch(InterruptedException e) {
  67. e.printStackTrace();
  68. }
  69. if(hotDogs.size() > 0) {
  70. System.out.println("消费者" + this.cid + "正在消费一个热狗,其编号为:
  71. " + hotDogs.remove(0));
  72. hotDogs.notifyAll();
  73. }else{
  74. try{
  75. System.out.println("已没有热狗,等待生产!");
  76. hotDogs.wait();
  77. }catch(InterruptedException e) {
  78. e.printStackTrace();
  79. }
  80. }
  81. }
  82. }
  83. }
  84. }
  85. }

  编译、运行程序,运行结果如图10.11所示。通过调整生产者和消费者模拟消耗的时间,重新编译、运行程序,程序运行结果会显示出符合需求的不同情况,大家可以尝试一下。

10.5 线程死锁和协作 - 图2


图10.11 生产者消费者问题