多线程(三)--线程间通信

本文介绍了线程间通信的重要性,以生产者和消费者问题为例,展示了如何通过等待/唤醒机制确保资源的有序处理。通过示例代码解释了wait(), notify()和notifyAll()的使用,并分析了多生产者多消费者场景下可能出现的数据错误和死锁问题,提出了解决方案。" 79760455,5564847,理解与实现后端接口幂等性,"['设计', '幂等性', '接口', '服务交互']

线程间通信

线程间通信的特征:多个线程在处理同一资源,但是任务不同,而且这些线程间的任务还会有一定的顺序。
例如生产者和消费者问题。必须得生产者生产了这个资源,消费者才能消费这个资源。下面以一个简单的例子来说明线程间通信:
例一:将姓名和性别封装成一个资源,两个不同的任务分别为输入和输出。

class Resource
{
    String name;
    String sex;
}

//输入
class Input implements Runnable
{
    Resource r;
    Input(Resource r)
    {
        this.r=r;
    }
    public void run()
    {
        int x=0;
        while(true)
        {
            synchronized(r)
            {
                if(x==0)
                {
                    r.name="mike";
                    r.sex="male";
                }
                else
                {
                    r.name="莉莉";
                    r.sex="female";
                }
            }
            x=(x+1)%2;
        }
    }
}
//输出
class Output implements Runnable
{
    Resource r;
    Output(Resource r)
    {
        this.r=r;
    }
    public void run()
    {
        while(true)
        {
            synchronized(r)
            {
                System.out.println(r.name+":"+r.sex);
            }
        }
    }
}

class ResourceDemo
{
    public static void main(String[] args)
    {
        //创建资源
        Resource r=new Resource();
        //创建任务
        Input in =new Input(r);
        Output out =new Output(r);
        //创建线程,添加路径
        Thread t1=new Thread(in);
        Thread t2=new Thread(out);
        //开启线程
        t1.start();
        t2.start();
    }
}

部分输出结果:

mike:male
mike:male
mike:male
mike:male
莉莉:female
莉莉:female
莉莉:female

通过结果可以看出,姓名和性别属性是对应的,这表示输入的资源输出无误。但是以上代码并不能保证输入一个资源及时输出一个资源(即生产一个资源就输出一个资源),这里以“Mike,male”和“莉莉,female”交替输入和输出来表示依次生产一个资源再消费这个资源。要想保证输入的资源不被覆盖,都能被输出,可以定义一个标记来判断是否有资源存在,有的话就输出,没有的话就输入。

等待/唤醒机制

1,wait():让线程处于冻结状态,线程释放执行权和执行资格。被wait的线程会被存储在线程池中。
2,notify();唤醒线程池中的一个线程(任意)。
3,notifyAll();唤醒线程池中的所有线程,让线程具备执行资格。

这些方法都必须定义在同步中,因为这些方法是用于操作线程状态的方法。必须要明确到底操作的是哪个锁上的线程。

为什么操作线程的方法wait,notify,notifyAll定义在了Object类中?
因为这些方法是监视器的方法。监视器其实就是锁。锁可以是任意的对象,任意的对象的调用方式一定定义在Object类中。
下面看例二:

//资源
class Resource
{
    String name;
    String sex;
    boolean flag=false;
}

//输入
class Input implements Runnable
{
    Resource r;
    Input(Resource r)
    {
        this.r=r;
    }
    public void run()
    {
        int x=0;
        while(true)
        {
            synchronized(r)
            {
                if(r.flag)
                    try{r.wait();}catch(InterruptedException e){}
                if(x==0)
                {
                    r.name="mike";
                    r.sex="male";
                }
                else
                {
                    r.name="莉莉";
                    r.sex="female";
                }
                r.flag=true;
                r.notify();
            }
            x=(x+1)%2;
        }
    }
}

//输出
class Output implements Runnable
{
    Resource r;
    Output(Resource r)
    {
        this.r=r;
    }
    public void run()
    {
        while(true)
        {
            synchronized(r)
            {
                if(!r.flag)
                    try {
                        r.wait();
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                System.out.println(r.name+":"+r.sex);
                r.flag=false;
                r.notify();
            }
        }
    }
}

class SourceDemo2
{
    public static void main(String[] args)
    {
        //创建资源
        Resource r=new Resource();
        //创建任务
        Input in =new Input(r);
        Output out =new Output(r);
        //创建线程,添加路径
        Thread t1=new Thread(in);
        Thread t2=new Thread(out);
        //开启线程
        t1.start();
        t2.start();
    }
}

在例二中,通过等待唤醒/机制,解决了例一存在的问题。当标记flag为false时,表示没有输出,此时执行输入任务,执行完后,flag变成true,输入线程进入休眠状态,输入线程进入该对象的线程池中,唤醒输出线程来执行输出任务;输出线程执行输出任务后,flag变成false,输出线程唤醒输入线程并进入休眠状态,输出线程进入该对象的线程池中。通过这样的一个顺序,就可以实现输入的任务会被依次输出而不被覆盖。
例二输出结果:

mike:male
莉莉:female
mike:male
莉莉:female
mike:male
莉莉:female
mike:male
莉莉:female

将资源的属性私有化,并对外提供方法后,最终优化代码得到例三:

class Resource
{
    private String name;
    private String sex;
    private boolean flag=false;
    public synchronized void set(String name,String sex)
    {
        if(flag)
            try{this.wait();}catch(InterruptedException e){}
        this.name=name;
        this.sex=sex;
        flag=true;
        notify();
    }
    public synchronized void out()
    {
        if(!flag)
            try{this.wait();}catch(InterruptedException e){}
        System.out.println(name+"..."+sex);
        flag=false;
        notify();
    }
}

//输入
class Input implements Runnable
{
    Resource r;
    Input(Resource r)
    {
        this.r=r;
    }
    public void run()
    {
        int x=0;
        while(true)
        {

            if(x==0)
            {
                r.set("mike","male");

            }
            else
            {
                r.set("lily","female");
            }

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

//输出
class Output implements Runnable
{
    Resource r;
    Output(Resource r)
    {
        this.r=r;
    }
    public void run()
    {

        while(true)
        {
            r.out();
        }
    }
}

class SourceDemo3
{
    public static void main(String[] args)
    {
        //创建资源
        Resource r=new Resource();
        //创建任务
        Input in =new Input(r);
        Output out =new Output(r);
        //创建线程,添加路径
        Thread t1=new Thread(in);
        Thread t2=new Thread(out);
        //开启线程
        t1.start();
        t2.start();
    }
}

多生产者多消费者

先来看一个例子:
例四:

class Resource
{
    private String name;
    private int count=1;
    private boolean flag=false;
    public synchronized void set(String name)
    {
        if(flag)
            try{this.wait();}catch(InterruptedException e){}
        this.name=name+count;
        count++;
        System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);
        flag=true;
        notify();
    }
    public synchronized void out()
    {
        if(!flag)
            try{this.wait();}catch(InterruptedException e){}
        System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name);
        flag=false;
        notify();
    }
}
class Producer implements Runnable
{
    private Resource r;
    Producer(Resource r)
    {
        this.r=r;
    }
    public void run()
    {
        while(true)
        {
            r.set("duck");
        }
    }
}

class Consumer implements Runnable
{
    private Resource r;
    Consumer(Resource r)
    {
        this.r=r;
    }
    public void run()
    {
        while(true)
            r.out();
    }
}

class ProducerConsumerDemo
{
    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();
    }
}

在例四中,有两个生产者,两个消费者,生产资源duck,每个资源duck都有自己的编号。
部分输出的结果:

Thread-2...消费者...duck49659
Thread-3...消费者...duck49659
Thread-2...消费者...duck49659
Thread-1...生产者...duck49660
Thread-0...生产者...duck49661

或者:

Thread-1...生产者...duck49750
Thread-3...消费者...duck49750
Thread-1...生产者...duck49751
Thread-3...消费者...duck49751
Thread-1...生产者...duck49752
Thread-3...消费者...duck49752

第二部分展示的结果是我们所希望看到的正确结果。第一部分的结果是异常的结果,是我们所不希望得到的。为什么会出现多个消费者多次消费一个资源duck49659,或者生产者生产的资源duck49660没有被消费到的情况?
从代码中分析:假设一开始线程0得到执行权,进入了set方法,此时flag为false,生产duck1,flag改为true,线程0进入休眠,此时notify没有意义。然后假设线程1得到执行权,由于flag为真,也进入休眠状态。再线程2得到执行权,消费duck1,flag改为false,线程2进入休眠。此时notify线程池中的线程0或者1,假设唤醒线程0。此时可以得到执行权的线程有线程0或者线程3。假设线程3获得执行权,此时由于flag为false,所以线程3也进入休眠。此时notify线程1或者线程2,假设唤醒了线程1。
这个时候醒着的线程为线程0和线程1。假设此时线程0获得执行权,由于线程0和1都已经进入set函数中,再进入休眠的,所以当他们醒过来的时候不会再进行标志判断。线程0生产了duck2,flag改为true,由于此时线程池中有线程2和3,线程0唤醒线程2,线程0进入休眠。假设此时获得执行权的是线程1,由于它已经进入set函数,不用进行flag的判断,所以线程1生产了duck3,这样子便会出现duck2没有被消费到。
同理也可以分析出同一个资源被多次消费的异常情况。以上的分析可以知道,会出现资源没被消费到或者同一资源被多次消费的情况是因为线程进入函数休眠后醒来没有判断标志位。如果判断了标志位就不会发生以上的异常情况。
怎样才能保证每次线程获得执行权后执行任务之前都进行标志位的判断呢?
可以用while代替if,这样便会使线程轮询标志位了。
修改后代码如下:

class Resource
{
    private String name;
    private int count=1;
    private boolean flag=false;
    public synchronized void set(String name)
    {
        while(flag)
            try{this.wait();}catch(InterruptedException e){}
        this.name=name+count;
        count++;
        System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);
        flag=true;
        notify();
    }
    public synchronized void out()
    {
        while(!flag)
            try{this.wait();}catch(InterruptedException e){}
        System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name);
        flag=false;
        notify();
    }
}
class Producer implements Runnable
{
    private Resource r;
    Producer(Resource r)
    {
        this.r=r;
    }
    public void run()
    {
        while(true)
        {
            r.set("duck");
        }
    }
}

class Consumer implements Runnable
{
    private Resource r;
    Consumer(Resource r)
    {
        this.r=r;
    }
    public void run()
    {
        while(true)
            r.out();
    }
}

class ProducerConsumerDemo2
{
    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();
    }
}

这样输出变成了

Thread-0...生产者...duck1
Thread-3...消费者...duck1
Thread-1...生产者...duck2
Thread-3...消费者...duck2
Thread-0...生产者...duck3

会发现多次消费同一资源或者生产的资源没有被消费的情况消失了。但会造成死锁,上面的结果就是由于死锁只得到了这些输出而程序却一直在运行。为什么会发生死锁?
分析:假设一开始线程0得到执行权,此时flag为false,生产了duck1,线程0进入休眠,flag变成了true。再线程1得到执行权,判断标志位为真,进入休眠。此时线程2得到执行权,消费duck1,进入休眠,唤醒了线程0,flag改为false。再线程3得到执行权,判断标志位为假,进入休眠,唤醒了线程2。线程0获得执行权,生产duck2,flag变成真,唤醒了线程1。线程1获得执行权,flag为真,进入休眠。此时只有线程2是醒着的。线程2获得执行权,flag为真,消费duck2,标志位改为假,进入休眠,唤醒了线程3。线程3判断标志位为假,进入休眠。
这样一来,四个线程全部进入冻结状态,没有线程来唤醒,因此进入了死锁状态。
从以上的分析可以看出,出现死锁是因为线程notify唤醒的是本方的线程,即执行同一任务的线程。只要唤醒的是对方的线程,就不会出现死锁情况。
解决办法即:用notifyAll方法将全部线程都唤醒即可。
修改后代码如下:

class Resource
{
    private String name;
    private int count=1;
    private boolean flag=false;
    public synchronized void set(String name)
    {
        while(flag)//用while可以每次唤醒一个线程后都进行判断,避免了有时候生产的没有被消费到的情况,或者生产一个被多次消费的情况。
            try{this.wait();}catch(InterruptedException e){}
        this.name=name+count;
        count++;
        System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);
        flag=true;
        notifyAll();//这样可以解决死锁问题(全部线程进入等待状态,没有线程来唤醒)
    }
    public synchronized void out()
    {
        while(!flag)
            try{this.wait();}catch(InterruptedException e){}
        System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name);
        flag=false;
        notifyAll();
    }
}
class Producer implements Runnable
{
    private Resource r;
    Producer(Resource r)
    {
        this.r=r;
    }
    public void run()
    {
        while(true)
        {
            r.set("duck");
        }
    }
}

class Consumer implements Runnable
{
    private Resource r;
    Consumer(Resource r)
    {
        this.r=r;
    }
    public void run()
    {
        while(true)
            r.out();
    }
}

class ProducerConsumerDemo3
{
    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();
    }
}

小结:
if判断标记,只有一次,会导致不该运行的线程运行了。出现数据错误的情况。
while判断标记,解决了线程获取执行权后,是否应该运行还是wait的问题。
notify只能唤醒一个线程,如果本方唤醒本方,没有意义。而while判断标记+notify会导致死锁。
notifyAll解决了本方线程一定会唤醒对方线程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值