34-多线程--死锁+线程间通信+等待唤醒机制+多生产者多消费者问题

一、死锁

1、死锁的常见情形之一:同步的嵌套

说明:同步的嵌套,至少得有两个锁,且第一个锁中有第二个锁,第二个锁中有第一个锁。eg:同步代码块中有同步函数,同步函数中有同步代码块。下面的例子,同步代码块的锁是obj,同步函数的锁是this。t1线程先执行同步代码块,获取锁obj,需要锁this才能执行同步函数;而t2线程先执行同步函数,获取锁this,需要锁obj才能执行同步代码块。两个线程相互竞争锁资源,可能和谐的交替执行到最后,也可能会发生死锁

死锁示例:t1线程执行 if 中的内容,先获取obj锁,再获取this锁,接着执行show()方法中的内容。之后出show(),释放this锁,但还没有出同步代码块,没有释放obj锁。此时,t2线程开始执行,走 else 中的内容,拿到this锁,想要获取obj锁。但obj锁被t1线程持有,造成死锁:t1线程持有obj锁,想要this锁;而t2线程持有this锁,想要obj锁。程序挂在这里运行不了

class Ticket implements Runnable {
    //票
    private int num = 400;
    //同步锁
    private Object obj = new Object();
    //标志位
    boolean flag = true;

    @Override
    public void run() {
        if (flag) {
            while (true) {
                //同步代码块中有同步函数
                //同步代码块,锁是obj
                synchronized (obj) {
                    //同步函数,锁是this
                    show();
                }
            }
        } else {
            while (true) {
                this.show();
            }
        }
    }

    /**
     * 同步函数,锁是this
     * 同步函数中有同步代码块
     */
    public synchronized void show() {
        //同步代码块,锁是obj
        synchronized (obj) {
            if (num > 0) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "......" + num--);
            }
        }
    }
}

public class Test {
    public static void main(String[] args) {
        Ticket t = new Ticket();

        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);

        t1.start();
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.flag = false;
        t2.start();
    }
}

2、要避免死锁的发生。不容易找到问题在哪儿,发现问题也不好解决

3、写死锁遵循的原则只有一个:嵌套

class Lock {
    /**
     * static:静态的,类名直接调用即可
     * final:锁固定不变
     * 嵌套至少得有两个锁
     */
    public static final Object loakA = new Object();
    public static final Object loakB = new Object();
}

class DeadLock implements Runnable {
    //标志位
    private boolean flag;

    //通过构造函数给标志位赋值
    //否则得在主函数中做切换,还得sleep(time),麻烦(见上例)
    DeadLock(boolean flag) {
        this.flag = flag;
    }

    @Override
    public void run() {
        if (flag) {
            //加 while循环 是为了让代码多执行几次,以便出现想要的结果
            while (true) {
                synchronized (Lock.loakA) {
                    System.out.println(Thread.currentThread().getName() + "...if...lockA...");
                    synchronized (Lock.loakB) {
                        System.out.println(Thread.currentThread().getName() + "...if...lockB...");
                    }
                }
            }
        } else {
            while (true) {
                synchronized (Lock.loakB) {
                    System.out.println(Thread.currentThread().getName() + "...else...lockB...");
                    synchronized (Lock.loakA) {
                        System.out.println(Thread.currentThread().getName() + "...else...lockA...");
                    }
                }
            }
        }
    }
}

public class Test {
    public static void main(String[] args) {
        /**
         * 1、用构造函数给标志位赋值,做标志位的切换
         *    主函数中就不用 切换+sleep(time) 了
         *
         * 2、线程任务是一个对象,多个线程操作同一个线程任务
         *    此处new两个线程任务对象a、b,两个线程t1、t2执行两个线程任务a、b,没问题
         *    因为两个线程任务a、b运行的代码都是run()方法,虽然两个对象都有自己的flag,但它们的flag值是固定的,一个是true,一个是false
         *    只有这种情况,可以封装多个任务
         *
         * 3、线程任务封装的资源是布尔型的变量,这个变量虽然在两个线程任务对象中都有独立的一份,但取的值就只能是true或false
         *    如果变量不是布尔型,是int,就导致有两个线程任务

         * (此处可以封装多个线程任务,是因为DeadLock中除了flag没有其他变量,且flag的值也是固定的(构造函数传参)
         *    只有这种情况才可以封装多个线程任务)
         */
        DeadLock dl1 = new DeadLock(true);
        DeadLock dl2 = new DeadLock(false);

        Thread t1 = new Thread(dl1);
        Thread t2 = new Thread(dl2);

        t1.start();
        t2.start();
    }
}

二、线程间通信

1、线程间通信:多个线程在处理同一资源,但线程任务不同(之前卖票和存钱的示例,都是多个线程在执行同一个线程任务,即 只有一个run()方法)

2、需求:有一个资源Rsoource,里面有两个属性name和sex。希望输入和输出轮流且不重复进行,即输入一次,输出一次;再输入一次,再输出一次......

(1)代码示例

/**
 * 创建一个类来描述资源
 * 资源是共享的,操作资源中属性(共享数据)的语句有多条,就存在线程安全问题
 */
class Resource {
    String name;
    String sex;
    //标志位,标记资源中有无数据。默认资源中没有数据,flag=false
    //规则:没有数据,就输入;有数据,就输出
    //所以,输入和输出前,都要先判断资源中有无数据
    boolean flag = false;
}

/**
 * 两个任务对象run()要分别封装在两个类中
 * 要封装线程任务,需要实现Runnable接口
 */
class Input implements Runnable {
    //此处不能new Resource(),否则输入和输出两个线程用的不是同一个资源
    //只能将资源Resource r作为参数传递进来。能接收参数传递的两种形式:一般方法和构造函数
    //因为线程任务对象一初始化就有资源,所以,使用构造函数传递参数Resource r
    private Resource r;

    Input(Resource r) {
        this.r = r;
    }

    @Override
    public void run() {
        //此处使用 int x 变量模拟多用户的切换:%2
        //还可以使用 boolean flag 变量做切换,flag = !flag
        int x = 0;
        while (true) {
            //为了保证输入和输出线程使用同一个锁,可以用Resource r作为锁,也可以用静态锁Resource.class等
            //锁不能用this,因为this代表本类对象,而输入和输出两个线程在两个类中
            synchronized (r) {
                //输入前,要先判断资源中有无数据
                //如果有数据,不用再输入。此时,Input要停一下,等待Output先输出
                //使用r.flag只代表flag是Resource r中的一个属性,可以省略r.
                if (r.flag) {
                    try {
                        //Input被wait()后,处于冻结状态,释放执行权的同时释放执行资格,只能等待被notify()唤醒
                        //使用wait()、notify()、notifyAll()方法时,必须要明确自己所属的锁,否则会报错
                        r.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                
                //如果没有数据,输入
                if (x == 0) {
                    r.name = "mike";
                    r.sex = "nan";
                } else {
                    r.name = "丽丽";
                    r.sex = "女";
                }
                //修改标志位。此时,资源中有数据了
                //使用r.flag只代表flag是Resource r中的一个属性,可以省略r.
                r.flag = true;
                //唤醒输出Output线程(因为Input即将进入等待)
                //如果Output没有进入等待状态,可以是空唤醒一次
                //使用wait()、notify()、notifyAll()方法时,必须要明确自己所属的锁,否则会报错
                r.notify();

                //模拟多用户的切换
                x = (x + 1) % 2;    //等价于:x = x++ % 2;
            }
        }
    }
}

/**
 * 两个任务对象run()要分别封装在两个类中
 * 要封装线程任务,需要实现Runnable接口
 */
class Output implements Runnable {
    //此处不能new Resource(),否则输入和输出两个线程用的不是同一个资源
    //只能将资源Resource r作为参数传递进来。能接收参数传递的两种形式:一般方法和构造函数
    //因为线程任务对象一初始化就有资源,所以,使用构造函数传递参数Resource r
    private Resource r;

    Output(Resource r) {
        this.r = r;
    }

    @Override
    public void run() {
        while (true) {
            //为了保证输入和输出线程使用同一个锁,可以用Resource r作为锁。也可以用静态锁Resource.class等
            //锁不能用this,因为this代表本类对象,而输入和输出两个线程在两个类中
            synchronized (r) {
                //输出前,要先判断资源中有无数据
                //如果没有数据,不用再输出。此时,Output要停一下,等待Input先输入
                //使用r.flag只代表flag是Resource r中的一个属性,可以省略r.
                if (!r.flag) {
                    try {
                        //Output被wait()后,处于冻结状态,释放执行权的同时释放执行资格,只能等待被notify()唤醒
                        //使用wait()、notify()、notifyAll()方法时,必须要明确自己所属的锁,否则会报错
                        r.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                
                //如果有数据,输出
                System.out.println(r.name + "......" + r.sex);

                //修改标志位。此时,资源中没有数据了
                //使用r.flag只代表flag是Resource r中的一个属性,可以省略r.
                r.flag = false;
                //唤醒输入Input线程(因为Output即将进入等待)
                //使用wait()、notify()、notifyAll()方法时,必须要明确自己所属的锁,否则会报错
                r.notify();
            }
        }
    }
}

public class Test {
    public static void main(String[] args) {
        //创建资源
        Resource r = new Resource();

        //创建线程任务。创建任务要明确资源
        Input input = new Input(r);
        Output output = new Output(r);

        //创建线程。创建线程要明确任务
        Thread t1 = new Thread(input);
        Thread t2 = new Thread(output);

        //开启线程
        t1.start();
        t2.start();
    }
}

分析:Input和Output共用资源Resource,所以,操作Resource中属性的多条语句需要加同步。主线程首先开启Input,执行其run()方法。判断资源中没有数据flag=false,向其中输入。之后将flag置为true,表示资源中已经有数据了。同时,唤醒Output(Input马上要进入冻结状态了)。此次循环结束,进入下次循环(while(true){...})。判断flag=true,执行wait()方法,Input被冻结。等到Output获取到CPU的执行权,Output执行。先判断flag=true,表示资源中有数据,输出。然后,将flag置为false,表示资源中没有数据。同时,唤醒Input(Output马上要进入冻结状态了)。此次循环结束,进入下次循环(while(true){...})。判断flag=false,执行wait()方法,Output被冻结。等到Input获取到CPU的执行权,再次重复上述操作......

注:创建线程,要明确任务;创建任务,要明确资源

(2)优化后的代码

class Resource {
    //资源的属性通常都是私有的,并对外提供访问方法
    private String name;
    private String sex;
    private boolean flag = false;

    //不单独写setName()、getName()方法,因为准备同时赋值
    //使用同步函数,用this锁更简单
    public synchronized void set(String name, String sex) {
        if (flag) {
            try {
                //此时使用的锁是this,代表资源Resource对象
                //wait()和notify()方法一定要所属于同步。因为它本身是锁上的方法,用来操作指定锁上的线程的方法
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        this.name = name;
        this.sex = sex;

        flag = true;
        //wait()和notify()方法一定要所属于同步。因为它本身是锁上的方法,用来操作指定锁上的线程的方法
        //this可省略
        this.notify();
    }

    //使用同步函数,用this锁更简单
    public synchronized void out() {
        if (!flag) {
            try {
                //wait()和notify()方法一定要所属于同步。因为它本身是锁上的方法,用来操作指定锁上的线程的方法
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println(name + "......" + sex);

        flag = false;
        //wait()和notify()方法一定要所属于同步。因为它本身是锁上的方法,用来操作指定锁上的线程的方法
        this.notify();
    }
}

class Input implements Runnable {
    private Resource r;

    Input(Resource r) {
        this.r = r;
    }

    @Override
    public void run() {
        int x = 0;
        while (true) {
            if (x == 0) {
                r.set("mike", "nan");
            } else {
                r.set("丽丽", "女");
            }

            x = (x + 1) % 2;
        }
    }
}

class Output implements Runnable {
    private Resource r;

    Output(Resource r) {
        this.r = r;
    }

    @Override
    public void run() {
        while (true) {
            r.out();
        }
    }
}

public class Test {
    public static void main(String[] args) {
        Resource r = new Resource();

        Input input = new Input(r);
        Output output = new Output(r);

        Thread t1 = new Thread(input);
        Thread t2 = new Thread(output);

        t1.start();
        t2.start();
    }
}

说明:资源里面封装了属性(私有的),同时对外提供了访问资源的方法。如果需要同步,在资源所提供的方法中加上同步即可

注:wait()和notify()方法一定要所属于同步。因为它本身是锁上的方法,用来操作指定锁上的线程的方法

三、等待唤醒机制

1、等待/唤醒机制涉及的方法

(1)wait():让线程处于冻结状态,被wait()的线程会被存储到线程池中(被wait()的线程没有消亡,但失去了CPU的执行资格,存储在线程池中)

注:线程池按照锁来区分

(2)notify():唤醒线程池中的任意一个线程

(3)notifyAll():唤醒线程池中的所有线程(让线程池中的线程都处于运行状态或者临时阻塞状态,即 让线程池中的线程具备CPU的执行资格)

2、使用wait()、notify()、notifyAll()方法的注意事项

(1)wait()、notify()、notifyAll()这些方法都必须定义在同步synchronized(对象){...}中。因为这些方法是用于操作线程状态的方法(监视线程的状态),一旦线程状态发生改变,必须要明确改变的是哪个锁上的线程。所以,在调用这些方法时,要标识出该方法所属的锁

(2)使用wait()、notify()、notifyAll()方法时,必须要明确自己所属的锁,否则会报错

(3)wait()、notify()、notifyAll()不是线程类Thread中的方法,而是Object中的方法。因为当前线程必须拥有此对象监视器,就是锁。而锁可以是任意的对象,任意的对象调用的方法一定定义在Object类中

四、多生产者多消费者问题

       生产者生产烤鸭,消费者消费烤鸭,且烤鸭带编号

1、需求:一个生产者和一个消费者,每次 生产/消费 一只烤鸭。只有没有烤鸭时才生产,只有有烤鸭时才消费,其余时间均等待

class Resource {
    private String name;
    private int count = 1;
    //标记
    private boolean flag = false;

    //传入:名称,得到:名称+编号
    //可以使用this锁,所以,使用同步代码块较简单
    public synchronized void set(String name) {
        if (flag) {
            try {
                //使用wait()、notify()、notifyAll()要明确所属的锁
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        this.name = name + count;
        count++;
        System.out.println(Thread.currentThread().getName() + "...生产者..." + this.name);

        this.flag = true;
        //使用wait()、notify()、notifyAll()要明确所属的锁
        this.notify();
    }

    public synchronized void out() {
        if (!flag) {
            try {
                //使用wait()、notify()、notifyAll()要明确所属的锁
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println(Thread.currentThread().getName() + "......消费者......" + this.name);

        this.flag = false;
        //使用wait()、notify()、notifyAll()要明确所属的锁
        this.notify();
    }
}

class Producer implements Runnable {
    private Resource r;

    Producer(Resource r) {
        this.r = r;
    }

    @Override
    public void run() {
        while (true){
            r.set("烤鸭");
        }
    }
}

class Consumer implements Runnable {
    private Resource r;

    Consumer(Resource r) {
        this.r = r;
    }

    @Override
    public void run() {
        while (true){
            r.out();
        }
    }
}

public class Test {
    public static void main(String[] args) {
        Resource r = new Resource();

        Producer pro = new Producer(r);
        Consumer con = new Consumer(r);

        Thread t1 = new Thread(pro);
        Thread t2 = new Thread(con);

        t1.start();
        t2.start();
    }
}

2、需求:两个生产者和两个消费者,每次 生产/消费 一只烤鸭。只有没有烤鸭时才生产,只有有烤鸭时才消费,其余时间均等待

public class Test {
    public static void main(String[] args) {
        Resource r = new Resource();

        Producer pro = new Producer(r);
        Consumer con = new Consumer(r);

        //线程封装任务,任务封装资源
        //两个生产者
        Thread t0 = new Thread(pro);
        Thread t1 = new Thread(pro);
        //两个消费者
        Thread t2 = new Thread(con);
        Thread t3 = new Thread(con);

        t0.start();
        t1.start();
        t2.start();
        t3.start();
   

问题一:当多个线程生产/消费时,出现了问题:生产一堆烤鸭未被消费,或多次消费同一编号的烤鸭

分析:

(1)生产者t0得到CPU的执行权,进入set()方法,判断flag=false,生产一只烤鸭(生产者...烤鸭1)。之后将flag置为true,执行notify()空唤醒,此次循环结束

(2)因为run()方法中的while(true),进入下次循环,t0再次执行set()方法。此时,flag=true,执行try代码块。t0被wait(),失去CPU的执行资格,进入线程池等待

(3)生产者t1得到CPU的执行权,进入set()方法,判断flag=true,执行try代码块。t1被wait(),失去CPU的执行资格,进入线程池等待

(4)消费者t2得到CPU的执行权,进入out()方法,判断flag=true,消费一只烤鸭(消费者...烤鸭1)。之后将flag置为false,执行notify(),唤醒生产者线程t0,此次循环结束

(5)因为run()方法中的while(true),进入下次循环,t2再次执行out()方法。此时,flag=false,执行try代码块。t2被wait(),失去CPU的执行资格,进入线程池等待

(6)消费者t3得到CPU的执行权,进入out()方法,判断flag=false,执行try代码块。t3被wait(),失去CPU的执行资格,进入线程池等待

(7)此时只有被唤醒的生产者t0还活着

(8)生产者t0得到CPU的执行权,从wait()处向下走(不再判断if(flag)),生产一只烤鸭(生产者...烤鸭2)。之后将flag置为true,执行notify(),唤醒生产者线程t1,此次循环结束

(9)因为run()方法中的while(true),进入下次循环,t0再次执行set()方法。此时,flag=true,执行try代码块。t0被wait(),失去CPU的执行资格,进入线程池等待

(10)此时只有被唤醒的生产者t1还活着

(11)生产者t1得到CPU的执行权,从wait()处向下走(不再判断if(flag)),生产一只烤鸭(生产者...烤鸭3)...... 出现问题:生产的烤鸭2未被消费就生产了烤鸭3

原因:被唤醒的线程没有再次判断 if(flag) 标记,就直接执行下面的代码,继续进行生产/消费

做法:将 if(flag) 改为 while(flag),让醒来的线程重新判断标志位flag

问题二:程序死锁

分析(从上面第(8)步开始):

(8)生产者t0得到CPU的执行权,从wait()处醒来。因为while(flag),会再次判断flag=false,生产一只烤鸭(生产者...烤鸭2)。之后将flag置为true,执行notify(),唤醒生产者线程t1,此次循环结束

(9)因为run()方法中的while(true),进入下次循环,t0再次执行set()方法。此时,flag=true,执行try代码块。t0被wait(),失去CPU的执行资格,进入线程池等待

(10)此时只有被唤醒的生产者t1还活着

(11)生产者t1得到CPU的执行权,从wait()处醒来。因为while(flag),会再次判断flag=true,执行try代码块。t1被wait(),失去CPU的执行资格,进入线程池等待

(12)此时,已经没有活着的线程了,所有线程都被冻结,在线程池等待

原因:notify()唤醒的是任意一方,没有对方导致程序死锁

做法:用notifyAll(),全唤醒(必须要唤醒对方)

正确的代码:

class Resource {
    private String name;
    private int count = 1;
    //标志位
    private boolean flag = false;

    //传入:名称,得到:名称+编号
    //可以使用this锁,所以,使用同步代码块较简单
    public synchronized void set(String name) {
        //用 while 而不是 if ,是为了让醒来的线程重新判断标志位
        while (flag) {
            try {
                //使用wait()、notify()、notifyAll()要明确所属的锁
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        this.name = name + count;
        count++;
        System.out.println(Thread.currentThread().getName() + "...生产者..." + this.name);

        this.flag = true;
        //使用wait()、notify()、notifyAll()要明确所属的锁
        //全唤醒,避免死锁
        this.notifyAll();
    }

    public synchronized void out() {
        //用 while 而不是 if ,是为了让醒来的线程重新判断标志位
        while (!flag) {
            try {
                //使用wait()、notify()、notifyAll()要明确所属的锁
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println(Thread.currentThread().getName() + "......消费者......" + this.name);

        this.flag = false;
        //使用wait()、notify()、notifyAll()要明确所属的锁
        //全唤醒,避免死锁
        this.notify();
    }
}

class Producer implements Runnable {
    private Resource r;

    Producer(Resource r) {
        this.r = r;
    }

    @Override
    public void run() {
        while (true) {
            r.set("烤鸭");
        }
    }
}

class Consumer implements Runnable {
    private Resource r;

    Consumer(Resource r) {
        this.r = r;
    }

    @Override
    public void run() {
        while (true) {
            r.out();
        }
    }
}

public class Test {
    public static void main(String[] args) {
        //线程封装任务,任务封装资源
        //资源
        Resource r = new Resource();
        
        //线程任务
        Producer pro = new Producer(r);
        Consumer con = new Consumer(r);
        
        //线程
        //两个生产者
        Thread t0 = new Thread(pro);
        Thread t1 = new Thread(pro);
        //两个消费者
        Thread t2 = new Thread(con);
        Thread t3 = new Thread(con);

        t0.start();
        t1.start();
        t2.start();
        t3.start();
    }
}

总结:

(1)while:判断标记,多次判断。解决了线程获取执行权后,是否要运行的问题

(2)notifyAll():唤醒全部。保证本方线程一定会唤醒对方线程,避免死锁

(3)if:判断标记,只判断一次。导致不该运行的线程运行,出现了数据错误的情况

(4)notify():只能唤醒一个线程。如果本方唤醒了本方,没有意义。而且 while判断标记+notify() 会导致死锁

依旧存在的问题:

       用 while判断标记+notifyAll() ,程序效率有点低。全唤醒时,本方也醒了,而本方判断标记没有意义。只唤醒对方即可

猜你喜欢

转载自blog.csdn.net/ruyu00/article/details/83501301