10.4 线程死锁和协作

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

10.4.1 线程死锁

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

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

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

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

public class DeadLockThread{

    //创建两个线程之间竞争使用的对象

    private static Object lock1 = new Object();

    private static Object lock2 = new Object();

    public static void main(String[] args){

        new Thread(new ShareThread1()).start();

        new Thread(new ShareThread2()).start();

    }

    private static class ShareThread1 implements Runnable

    {

        public void run(){

            synchronized(lock1){

                try{

                    Thread.sleep(50);

                }catch(InterruptedException e)

                {

                    e.printStackTrace();

                }

                synchronized(lock2){

                    System.out.println("ShareThread1");

                }

                }

            }

        }

        private static class ShareThread2 implements Runnable

        {

            public void run(){

                synchronized(lock2){

                try{

                    Thread.sleep(50);

                }catch(InterruptedException e)

                {

                    e.printStackTrace();

                }

                synchronized(lock1){

                    System.out.println("ShareThread2");
    
                }

            }

        }

    }

}
copy

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

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

10.4.2 产生死锁的原因及条件

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

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

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

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

  (3)资源分配不当等。

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

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

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

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

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

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

10.4.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.10 线程等待与唤醒

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

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

  实现此功能的代码如下:

import java.util.*;

public class TestProdCons {

    //定义一个存放热狗的集合,里面存放的是整数,代表热狗编号

    private static final List<Integer> hotDogs = new ArrayList<Integer>();

    public static void main(String[] args){

        for(int i = 1;i <= 3;i++){

            new Producer(i).start();

        }

        for(int i = 1;i <= 5;i++){

            new Consumer(i).start();

        }

        try{

            Thread.sleep(2000);

        }catch(InterruptedException e){

            e.printStackTrace();

        }

        System.exit(0);

    }

    //生产者线程,以热狗集合作为对象锁,所有对热狗集合的操作互斥

    private static class Producer extends Thread{

        int i = 1;

        int pid = -1;

        public Producer(int id){

            this.pid = id;

        }

        public void run(){

            while(true){

                try{

                    //模拟消耗的时间

                    Thread.sleep(100);

                } catch (InterruptedException e) {

                    e.printStackTrace();

                }

                synchronized(hotDogs){

                    if(hotDogs.size() < 10){

                        //热狗编号,300002代表编号为3的生产者生产的第2个热狗

                        hotDogs.add(pid*10000 + i);

                        System.out.println("生产者" + pid + "生产热狗,编号为:" + pid*10000 + i);

                        i++;

                        //唤醒hotDogs对象锁上所有调用wait()方法的线程

                        hotDogs.notifyAll();

                    }else{

                        try{

                            System.out.println("热狗数已到10个,等待消费!");

                            hotDogs.wait();

                        }catch(InterruptedException e) {

                            e.printStackTrace();

                        }

                }

            }

        }

    }

}

//消费者线程,以热狗集合作为对象锁,所有对热狗集合的操作互斥

private static class Consumer extends Thread {

    int cid = -1;

    public Consumer(int id){

        this.cid = id;

    }

    public void run(){

        while(true){

            synchronized (hotDogs) {

            try{

                //模拟消耗的时间

                Thread.sleep(200);

            }catch(InterruptedException e) {

                e.printStackTrace();

            }

            if(hotDogs.size() > 0) {

                System.out.println("消费者" + this.cid + "正在消费一个热狗,其编号为:

                " + hotDogs.remove(0));

                hotDogs.notifyAll();

            }else{

                try{

                    System.out.println("已没有热狗,等待生产!");

                    hotDogs.wait();

                }catch(InterruptedException e) {

                    e.printStackTrace();

                }

                }

            }

            }

        }

    }

}
copy

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

图10.11 生产者消费者问题