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

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

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

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

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

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

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

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

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

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

      (3)资源分配不当等。

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

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

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

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

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

      通过之前的学习,已经了解并初步解决了多线程之间可能出现的问题,下一步学习的重点是如何让线程之间进行有效协作。线程协作的一个典型案例就是生产者和消费者问题,生产者和消费者的这种协作是通过线程之间的握手来实现的,而这种握手又是通过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.10 线程等待与唤醒

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

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

      实现此功能的代码如下:

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

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

    10.5 线程死锁和协作 - 图2