Java 多线程编程5---线程操作案例:生产者和消费者

在线程操作中有一个经典的案例,即生产者和消费者问题。生产者不断生产,消费者不断取走生产者生产的产品。即生产者生产出的产品放到一个区域中,消费者从该区域中取走产品.如下图所示。

Created with Raphaël 2.1.2 生产者不断生产数据 数据存储空间 消费者不断取走数据

在上面的方式中,如果使用多线程运行,由于多线程运行的不确定性,会存在以下问题:

  • 假设生产者线程刚向数据存储空间中加入该信息的名称,还没有加入信息的内容,程序就切换到了消费者线程,消费者线程把信息的名称和上一个信息的内容联系到一起。
  • 生产者放入若干次数的数据,消费者才开始取出数据,
  • 或者是,消费者取完了一个数据后,还没等到生产者放入新的数据,又重复放入以取过的数据。

    生产者消费者的基本实现

    因为现在程序中生产者不断生产的是信息,而消费者不断取出的也是信息,所以这定义一个保存信息的类:Info.java

保存信息的类:Info.java

package my.thread.producer_consumer;

public class Info
{
    // 产品名称
    private String name;
    // 产品内容
    private String content;
    public void setName(String name)
    {
        this.name = name;
    }
    public void setContent(String content)
    {
        this.content = content;
    }
    public String getName()
    {
        return this.name;
    }
    public String getContent()
    {
        return this.content;
    }
}

Info类的中设置了两个属性:name,content分别表示产品的名称和产品的内容。因为生产者和消费者需要操作一个相同的数据存储区域(这里是一个Info类的对象)。所以生产者和消费者线程分别实现Runable接口,并在生产者和消费者实现类中传入Info类的对象以便操作。
生产者:

package my.thread.producer_consumer;

public class Producer implements Runnable
{
    // 产品信息
    private Info info = null;
    public Producer(Info info)
    {
        this.info = info;
    }
    public void run()
    {
        //生产两种产品:产品A和产品B
        boolean flag = false;
        for (int i = 0; i < 50; i++)
        {

            if (flag)
            {
                // 设置产品名称
                info.setName("产品A");
                try
                {
                    Thread.sleep(90);
                } catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
                info.setContent("产品A的内容"); // 设置内容
                //下次生产另一个产品,交替生产两个产品
                flag = false;
            } else
            {
                info.setName("产品B"); // 设置名称
                try
                {
                    Thread.sleep(90);
                } catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
                info.setContent("产品B的内容"); // 设置内容
                flag = true;
            }
        }
    }
}

生产者类的构造方法中传入了产品类Info的实例对象引用,然后在run()方法中循环10次以生产具体的信息,为了效果更加明显,在设置产品名称和产品内容之间的语句之间加入了延迟操作。
消费者:

package my.thread.producer_consumer;

public class Consumer implements Runnable
{
    private Info info = null;
    public Consumer(Info info)
    {
        this.info = info;
    }
    public void run()
    {
        for (int i = 0; i < 10; i++)
        {
            try
            {
                Thread.sleep(90);
            } catch (InterruptedException e)
            {
                e.printStackTrace();
            }
            System.out.println(
                    this.info.getName() + " --> " + this.info.getContent());
        }
    }
}

测试类:
消费者类线程类中也同样接收一个Info对象的引用,并采用循环方式取出10次信息,然后输出到控制台上。

package my.thread.producer_consumer;

public class ProducerConsumerTest
{
    public static void main(String args[])
    {
        Info info = new Info(); // 实例化Info对象
        Producer pro = new Producer(info); // 生产者
        Consumer con = new Consumer(info); // 消费者
        new Thread(pro).start();
        new Thread(con).start();
    }
}

运行结果:

产品A --> 产品B的内容
产品A --> 产品B的内容
产品B --> 产品A的内容
产品A --> 产品A的内容
产品A --> 产品B的内容
产品A --> 产品A的内容
产品B --> 产品A的内容
产品A --> 产品A的内容
产品A --> 产品B的内容
产品A --> 产品A的内容

从运行结果中看,出现了下面两个问题:

  1. 取出的产品名称为产品A,但是内容却是产品B的内容,这不符合要求,这是取出数据不匹配的问题。
  2. 本次取出的是产品A,下次应该取出的是产品B,但是却出现了下次取出的还是产品A的情况,这是重复取出数据的问题。

    解决取出数据不匹配的问题—加入同步机制

使用同步代码块

使用同步,我们目前知道有两个方法,一个是使用同步代码块,一个是使用同步方法。先来看使用同步代码块的例子。在生产者生产信息的代码处,和消费者取出信息的代码处分别使用同步代码块包裹起来即可。分别修改代码的生产者和消费者代码即可,修改的关键代码如下。
生产者需要同步的代码:

synchronized (info)
{
    // 设置产品名称
    info.setName("产品A");
    try
    {
        Thread.sleep(90);
    } catch (InterruptedException e)
    {
        e.printStackTrace();
    }
    info.setContent("产品A的内容"); // 设置内容
}

消费者需要同步的关键代码

synchronized (info)
{
    try
    {
        Thread.sleep(90);
    } catch (InterruptedException e)
    {
        e.printStackTrace();
    }
    System.out.println(
            this.info.getName() + " --> " + this.info.getContent());
}

完整的代码如下:
生产者:

package my.thread.producer_consumer.sync;

public class Producer implements Runnable
{
    // 产品信息
    private Info info = null;
    public Producer(Info info)
    {
        this.info = info;
    }
    public void run()
    {
        // 生产两种产品:产品A和产品B
        boolean flag = false;
        for (int i = 0; i < 10; i++)
        {

            if (flag)
            {
                synchronized (info)
                {
                    // 设置产品名称
                    info.setName("产品A");
                    try
                    {
                        Thread.sleep(90);
                    } catch (InterruptedException e)
                    {
                        e.printStackTrace();
                    }
                    info.setContent("产品A的内容"); // 设置内容
                }
                // 下次生产另一个产品,交替生产两个产品
                flag = false;
            } else
            {
                synchronized (info)
                {

                    info.setName("产品B"); // 设置名称
                    try
                    {
                        Thread.sleep(90);
                    } catch (InterruptedException e)
                    {
                        e.printStackTrace();
                    }
                    info.setContent("产品B的内容"); // 设置内容
                }
                flag = true;
            }
        }
    }
}

消费者:

package my.thread.producer_consumer.sync;

public class Consumer implements Runnable
{
    private Info info = null;
    public Consumer(Info info)
    {
        this.info = info;
    }
    public void run()
    {
        for (int i = 0; i < 10; i++)
        {
            synchronized (info)
            {
                try
                {
                    Thread.sleep(90);
                } catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
                System.out.println(
                        this.info.getName() + " --> " + this.info.getContent());
            }
        }
    }
}

测试类:

package my.thread.producer_consumer.sync;

import my.thread.producer_consumer.sync.Producer;
import my.thread.producer_consumer.sync.Consumer;

public class ProducerConsumerSyncBlockTest
{
    public static void main(String args[])
    {
        Info info = new Info(); // 实例化Info对象
        Producer pro = new Producer(info); // 生产者
        Consumer con = new Consumer(info); // 消费者
        new Thread(pro).start();
        new Thread(con).start();
    }
}

运行结果:

产品A --> 产品A的内容
产品A --> 产品A的内容
产品A --> 产品A的内容
产品A --> 产品A的内容
产品B --> 产品B的内容
产品A --> 产品A的内容
产品B --> 产品B的内容
产品A --> 产品A的内容
产品B --> 产品B的内容
产品A --> 产品A的内容

从运行结果可以发现,加入同步机制后,取出信息不匹配的问题得到解决了。这是因为设置和读取info这个对象使用了同步,进行设置和读取都需要获取info对象上的锁,在设置操作未完成之前,设置操作将一直持有info对象的锁,读取操作此时如果想读取的话,由于获取不到info对象的锁。只能等待设置操作设置完毕后才能读取,所以读取到的信息就不会出现不匹配的问题。而设置操作如果此时要从新设置新的内容也要获取info对象的锁,由于此时正在读取,设置操作要等待读取完成释放锁后才能设置,一句话来说,读取操作和修改操作是互斥的读取操作和修改操作对于info对象的操作是互斥的所以取出的信息不回出现不匹配的问题。

使用同步方法实现生产者消费者问题的同步

也可以使用同步方法实现同步,这样的话就需要把出现问题的代码抽出打包成方法即可。修改Info类,把设置产品名称和设置产品内容的两条设置语句打包成一个setInfo()方法即可。同时在生产者代码和消费者代码中也做相应的修改。
修改后的代码如下
修改后的Info类

package my.thread.producer_consumer.sync.fun;

public class Info
{
    // 产品名称
    private String name;
    // 产品内容
    private String content;
    private void setName(String name)
    {
        this.name = name;
    }
    private void setContent(String content)
    {
        this.content = content;
    }
    private String getName()
    {
        return this.name;
    }
    private String getContent()
    {
        return this.content;
    }
    /**   
     * 使用同步方法  同时设置产品的名称和产品的内容
     */  
    public synchronized void setInfo(String name,String content)
    {
        // 设置产品名称
        this.setName(name);
        try
        {
            Thread.sleep(90);
        } catch (InterruptedException e)
        {
            e.printStackTrace();
        }
        this.setContent(content); // 设置内容
    }
    public synchronized String[] getInfo()
    {
        try
        {
            Thread.sleep(90);
        } catch (InterruptedException e)
        {
            e.printStackTrace();
        }

        String[] produces=new String[2];
        produces[0]=this.getName();
        produces[1]=this.getContent();
        return produces;
    }
}

在上面的Info类中提供了两个同步方法setInfo()和getInfo(),实现对info对象的同步访问。然后把getters和setters方法统统设置成private权限,这样能避免在其他的类中使用info对象调用setters和getters方法对info中的属性做修改而造成信息错误。或者索性就删除getters和setters方法也可以。
修改后生产者代码:

package my.thread.producer_consumer.sync.fun;

public class Producer implements Runnable
{
    // 产品信息
    private Info info = null;
    public Producer(Info info)
    {
        this.info = info;
    }
    public void run()
    {
        //生产两种产品:产品A和产品B
        boolean flag = false;
        for (int i = 0; i < 10; i++)
        {

            if (flag)
            {
//              info.setName("产品A"); // 设置名称
//              try
//              {
//                  Thread.sleep(90);
//              } catch (InterruptedException e)
//              {
//                  e.printStackTrace();
//              }
//              info.setContent("产品A的内容"); // 设置内容
                info.setInfo("产品A", "产品A的内容");
                //下次生产另一个产品,交替生产两个产品
                flag = false;
            } else
            {
//              info.setName("产品B"); // 设置名称
//              try
//              {
//                  Thread.sleep(90);
//              } catch (InterruptedException e)
//              {
//                  e.printStackTrace();
//              }
//              info.setContent("产品B的内容"); // 设置内容
                info.setInfo("产品B", "产品B的内容");
                flag = true;
            }
        }
    }
}

修改后的消费者代码:

package my.thread.producer_consumer.sync.fun;

import my.thread.producer_consumer.sync.fun.Info;;

public class Consumer implements Runnable
{
    private Info info = null;
    public Consumer(Info info2)
    {
        this.info = info2;
    }
    public void run()
    {
        for (int i = 0; i < 10; i++)
        {
//          try
//          {
//              Thread.sleep(90);
//          } catch (InterruptedException e)
//          {
//              e.printStackTrace();
//          }
//          System.out.println(
//                  this.info.getName() + " --> " + this.info.getContent());
            String[] produces=info.getInfo();
            System.out.println(produces[0] + " --> " + produces[1]);
        }
    }
}

测试类代码保持不变

package my.thread.producer_consumer.sync.fun;

public class ProducerConsumerTest
{
    public static void main(String args[])
    {
        Info info = new Info(); // 实例化Info对象
        Producer pro = new Producer(info); // 生产者
        Consumer con = new Consumer(info); // 消费者
        new Thread(pro).start();
        new Thread(con).start();
    }
}

运行结果:

产品B --> 产品B的内容
产品A --> 产品A的内容
产品B --> 产品B的内容
产品A --> 产品A的内容
产品B --> 产品B的内容
产品A --> 产品A的内容
产品A --> 产品A的内容
产品B --> 产品B的内容
产品A --> 产品A的内容
产品A --> 产品A的内容

可以看到,使用同步方法也是也是可以解决信息不匹配的问题的。不过从上面的代码中也可以发现在消费者类中,获取信息使用数字实现,这样多了使用中间变量。使得代码占据了更多的空间,所以对比一下还是使用同步代码块块比使用同步方法更为简洁,和占据更少的内存空间。

使用同步之后虽然结局了产品信息不匹配的问题,但是依然存在读取到的信息重复的问题。使用等待和唤醒机制可以解决读取信息重复的问题,如下。

Object类对线程的支持—-等待与唤醒

Object类是所有类的父类,在此类中有一下几种方法对线程操作有所支持的,如下表所示。

序号Object中线程相关的方法描述
1void wait()当前线程等待。
2void wait(long timeout)当前线程等待。并指定等待的时间(毫秒)
3void wait(long timeout, int nanos)当前线程等待,并指定等待的毫秒,以及纳秒
4void notify()唤醒在此对象监视器上等待的单个线程。
5void notifyAll()唤醒在此对象监视器上等待的所有线程。

从上面的表中可以看出,可以将一个线程设置为等待状态,但是对于唤醒的操作却又两个,分别为notify()和notifyAll()。一般来说,所有等待的线程会按照顺序进行排列,如果此时使用了notify()方法,则会唤醒第一个等待的线程,第一个等待的线程得以执行,而如果使用了notifyAll()方法,则会唤醒所有的等待的线程,哪个线程优先级高,哪个线程就有可能先得到CPU调度而先执行。

解决生产者消费者重复问题—加入等待与唤醒机制

如果想让生产者不重复生产,消费者不重复取走,则可以增加一标准位,设置标志位为boolean变量,如果标着为的内容为true,这表示可以生产,但是不能取走,此时若执行到了消费者线程,则消费者线程应该等待。如果标志位的内容为false,则表示可以取走,但是此时不能生产,如果执行到了生产者线程,则生产者线程应该等待。操作流程如下所示。

Created with Raphaël 2.1.2 生产者线程进入 判断标志位 设置Info类的名称和内容 修改标志位 等待线程唤醒 等待消费者取走 yes no

生产者操作

Created with Raphaël 2.1.2 消费者线程进入 判断标志位 取走Info类对象的名称和内容 修改标志位 等待线程唤醒 等待生产者生产 yes no

消费者操作

要完成以上的功能,直接在 上面同步方法的例子的基础上修改一下Info类即可,在Info类中加入标志,并通过判断标志位完成等待与唤醒的操作,代码修改如下。
实例:修改Info类

package my.thread.producer_consumer.sync.fun;

public class Info
{
    // 产品名称
    private String name;
    // 产品内容
    private String content;
    //设最初标志位为true表示,一开始可以生产
    private boolean flag=true;
    private void setName(String name)
    {
        this.name = name;
    }
    private void setContent(String content)
    {
        this.content = content;
    }
    private String getName()
    {
        return this.name;
    }
    private String getContent()
    {
        return this.content;
    }
    /**   
     * 使用同步方法  同时设置产品的名称和产品的内容
     */  
    public synchronized void setInfo(String name,String content)
    {
        //如果标志位为假,则不能生产,生产线程等待
        if(!flag)
        {
            try
            {
                this.wait();
            } catch (InterruptedException e)
            {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        //如果标志位为true,则表明可以生产
        //开始生产...

        // 设置产品名称
        this.setName(name);
        try
        {
            Thread.sleep(90);
        } catch (InterruptedException e)
        {
            e.printStackTrace();
        }
        this.setContent(content); // 设置内容

        //生产结束,改变标签,表明消费者可以取走产品
        flag=false;
        //叫醒消费者
        this.notify();
    }
    public synchronized String[] getInfo()
    {
        //如果标志位为true则表明正在生产

        if(flag)
        {
            try
            {
                this.wait();
            } catch (InterruptedException e)
            {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        //如果flag为flase则表明生产结束了可以取走
        //开始取走产品
        try
        {
            Thread.sleep(90);
        } catch (InterruptedException e)
        {
            e.printStackTrace();
        }

        String[] produces=new String[2];
        produces[0]=this.getName();
        produces[1]=this.getContent();
        //取走产品结束,改变标志位通知生产者可以生产了
        flag=true;
        //叫醒生产者
        this.notify();
        return produces;
    }
}

其他代码不变,运行效果:

产品B --> 产品B的内容
产品A --> 产品A的内容
产品B --> 产品B的内容
产品A --> 产品A的内容
产品B --> 产品B的内容
产品A --> 产品A的内容
产品B --> 产品B的内容
产品A --> 产品A的内容
产品B --> 产品B的内容
产品A --> 产品A的内容

从运行结果来看,现在的生产者每生产一个产品,就会等待消费者取走,消费者没取走一个就要等待生产再次生产。这样就避免了重复生产和重复取走的问题。
上面的代码中使用了同步方法+等待唤醒机制解决了生产者消费者问题。下面来使用同步代码块+等待唤醒机制实现。

使用同步代码块+等待唤醒机制解决生产者消费者问题

要注意的是,必须要用锁对象(info)来调用notify()来唤醒在唤醒等待该锁的线程,这里的锁对象时info,所以调用方式为info.notify(),而且重要的一点是,一个线程要唤醒另一个线程则该线程必须先是该锁的持有者,然后通过该锁来唤醒另一个等待该锁的线程。通俗的讲,等待唤醒的调用方式为:

同步代码块的等待和唤醒的调用格式

持有当前锁对象的锁的线程,进入等待队列排队

synchronized (锁对象)
{
...
//持有该对象的锁的线程,放弃该锁,进入等待队列排队
锁对象.wait();
...
}

持有当前锁对象的锁的线程,放弃该锁,把它给等待队列中的最前面的一个线程

synchronized (锁对象)
{
...
//持有该对象的锁的线程,放弃该锁,进入等待队列排队
锁对象.notify();
...
}

持有当前锁对象的锁的线程,唤醒等待队列中的所有线程,队列中的线程来抢该锁,谁抢到谁运行

synchronized (锁对象)
{
...
//持有该对象的锁的线程,放弃该锁,进入等待队列排队
锁对象.notifyall();
...
}

要注意的是:

  • 在同步代码块中,调用wait(),notify(),notifyAll()方法
  • 要使用锁对象调用以上方法,用其他对象调用,会抛出异常java.lang.IllegalMonitorStateException

实例:同步代码块+等待唤醒机制解决生产者消费者问题
在上面的同步代码快的例子上修改。
1.修改后的Info类

package my.thread.producer_consumer.sync;

public class Info
{
    // 产品名称
    private String name;
    // 产品内容
    private String content;
    private boolean allowToProduce=true;
    public boolean isAllowToProduce()
    {
        return allowToProduce;
    }
    public void setAllowToProduce(boolean allowToProduce)
    {
        this.allowToProduce = allowToProduce;
    }
    public void setName(String name)
    {
        this.name = name;
    }
    public void setContent(String content)
    {
        this.content = content;
    }
    public String getName()
    {
        return this.name;
    }
    public String getContent()
    {
        return this.content;
    }
}

Info类没有做太多的修改,在Info类中加入一个标记private boolean allowToProduce=true;然后提供getters和setters放方法即可。
修改后的生产者类

package my.thread.producer_consumer.sync;

public class Producer implements Runnable
{
    // 产品信息
    private Info info = null;
    public Producer(Info info)
    {
        this.info = info;
    }
    public void run()
    {
        // 生产两种产品:产品A和产品B
        boolean flag = false;
        for (int i = 0; i < 10; i++)
        {
            if (flag)
            {
                synchronized (info)
                {
                    //如果不允许生产
                    if(!info.isAllowToProduce())
                    {
                        //那就等着呗
                        try
                        {
                            info.wait();
                        } catch (InterruptedException e)
                        {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                    }
                    //如果允许生产    
                    //那就生产呗 
                    // 设置产品名称
                    info.setName("产品A");
                    try
                    {
                        Thread.sleep(90);
                    } catch (InterruptedException e)
                    {
                        e.printStackTrace();
                    }
                    info.setContent("产品A的内容"); // 设置内容

                    //生产结束,修改标志,表示可以取走了
                    info.setAllowToProduce(false);
                    //唤醒消费者线程来取走
                    info.notify();


                }
                // 下次生产另一个产品,交替生产两个产品
                flag = false;
            } else
            {
                synchronized (info)
                {
                    //如果不允许生产
                    if(!info.isAllowToProduce())
                    {
                        //那就等着呗
                        try
                        {
                            info.wait();
                        } catch (InterruptedException e)
                        {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                    }
                    info.setName("产品B"); // 设置名称
                    try
                    {
                        Thread.sleep(90);
                    } catch (InterruptedException e)
                    {
                        e.printStackTrace();
                    }
                    info.setContent("产品B的内容"); // 设置内容

                    //生产结束,修改标志,表示可以取走了
                    info.setAllowToProduce(false);
                    //唤醒消费者线程来取走
                    info.notify();
                }
                flag = true;
            }
        }
    }
}

在生产者类中的同步代码块中根据标记分别加上等待唤醒机制即可。如果标记为真表示可以生产,生产完毕后唤醒消费者线程,如果标记为假,则生产者线程等待,注意一定要在同步代码块中,使用锁对象info来调用等待唤醒方法
消费者代码

package my.thread.producer_consumer.sync;

public class Consumer implements Runnable
{
    private Info info = null;
    public Consumer(Info info)
    {
        this.info = info;
    }
    public void run()
    {
        for (int i = 0; i < 10; i++)
        {
            synchronized (info)
            {
                // 如果允许生产,那就是不允许取走
                if (info.isAllowToProduce())
                {
                    // 那就等着呗
                    try
                    {
                        info.wait();
                    } catch (InterruptedException e)
                    {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
                //如果允许消费了
                try
                {
                    Thread.sleep(90);
                } catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
                System.out.println(
                        this.info.getName() + " --> " + this.info.getContent());

                //消费结束,修改标志,表示可以生产了
                info.setAllowToProduce(true);
                //唤醒消费者线程来取走
                info.notify();
            }
        }
    }
}

测试类:

package my.thread.producer_consumer.sync;

import my.thread.producer_consumer.sync.Producer;
import my.thread.producer_consumer.sync.Consumer;

public class ProducerConsumerSyncBlockTest
{
    public static void main(String args[])
    {
        Info info = new Info(); // 实例化Info对象
        Producer pro = new Producer(info); // 生产者
        Consumer con = new Consumer(info); // 消费者
        new Thread(pro).start();
        new Thread(con).start();
    }
}

运行结果:

产品B --> 产品B的内容
产品A --> 产品A的内容
产品B --> 产品B的内容
产品A --> 产品A的内容
产品B --> 产品B的内容
产品A --> 产品A的内容
产品B --> 产品B的内容
产品A --> 产品A的内容
产品B --> 产品B的内容
产品A --> 产品A的内容
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值