在线程操作中有个经典的案例程序,即生产者和消费者问题,生产者不断生产,消费者不断消费生产者生产的产品。
生产者生产出的信息方法一个区域之中,消费者从区域中将数据取出来,但是本程序中因为牵扯到线程运行的不确定性,所以会存在两个问题:
1. 假设生产者线程刚向数据空间添加了信息的名称,还没有加入该信息的内容,程序就切换到了消费者线程,消费者线程将把信息的名称和上一个信息的内容联系在一起。
2. 生产者放了若干次的数据,消费者才开始取数据,或者是,消费者取完一个数据后,还没等到生产者放入新的数据,又重复取出已经取出的数据。
- 程序的基本实现
因为程序中生产者不断生产的是信息,而消费者不断取出的也是信息,所以定义一个保存信息的类Info.java
【Info.java】
class Info
{
private String name = "张三";
private String content= "教师";
public String getName(){
return name;
}
public void setName(String name){
this.name=name;
}
public String getContent(){
return content;
}
public void setContent(String content){
this.content=content;
}
}
Info类的组成非常简单,只包含了用于保存信息名称的name属性和用于保存信息生产者的content属性,因为生产者和消费者要同时操作一个空间的内容,所以生产者和消费者分别实现Runnable接口,并接收Info类的引用。
【生成者】
class Producer implements Runnable
{
private Info info=null;
public Producer(Info info){
this.info=info;
}
public run(){
boolean flag=false;
for (int i=0;i<50 ;i++ )
{
if (flag)
{
this.info.setName("张三");
try
{
Thread.sleep(90);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
this.info.setContent("教师");
flag=false;
}else
{
this.info.setName("zhangsan");
try
{
Thread.sleep(90);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
this.info.setContent("www.zhangsan.com");
flag=true;
}
}
}
}
在生产者类的构造方法中传入了Info类的实例化对象,然后在run()方法中循环50次以产生信息的具体内容,此外为了让读者更好的发现问题,在线程中加入了延迟操作。
【消费者】
class Consumer implements Runnable
{
private Info info=null;
public Consumer (Info info){
this.info=info;
}
public void run(){
for (int i=0;i<50 ;i++ )
{
try
{
Thread.sleep(90);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
System.out.println(this.info.getName()+"-->"+this.info.getContent());
}
}
}
在消费者线程类中同样也接收了一个info对象的引用,并采用循环的方式取出50次信息并输出。
【测试程序】
public class ThreadCaseDemo01
{
public static void main(String args[])
{
Info info=new Info();
Producer pro=new Producer(info);
Consumer con=new Consumer(info);
new Thread(pro).start();
new Thread(con).start();
}
}
运行结果:
张三-->www.zhangsan.com
zhangsan-->教师
张三-->www.zhangsan.com
zhangsan-->教师
张三-->www.zhangsan.com
zhangsan-->教师
张三-->www.zhangsan.com
zhangsan-->教师
张三-->www.zhangsan.com
zhangsan-->教师
张三-->www.zhangsan.com
zhangsan-->教师
张三-->www.zhangsan.com
。。。。。
zhangsan-->教师
张三-->www.zhangsan.com
张三-->教师
张三-->www.zhangsan.com
张三-->教师
。。。。
从运行结果来看前面所提到的问题都出现了,下面先来解决第一个问题。
- 问题解决1——加入同步
如果要为操作加入同步,则可以通过定义同步方法的方式完成,即将设置名称和姓名定义成一个同步方法。
【修改Info类】
在info类中加入;
public synchronized void set(String name,String content){
this.setName(name);
try
{
Thread.sleep(300);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
this.setContent(content);
}
public synchronized void get(){
try
{
Thread.sleep(300);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
System.out.println(this,getName()+"-->"+this.getContent());
}
在类中加入了一个set()和get()方法,并使用synchronized关键字进行声明,因为现在不希望直接调用getter及setter方法,所以修改生产者和消费者类中的代码就行了
【修改生产者】
class Producer implements Runnable
{
private Info info=null;
public Producer(Info info){
this.info=info;
}
public void run(){
boolean flag=false;
for (int i=0;i<50 ;i++ )
{
if (flag)
{
this.info.set("张三","教师");
flag=false;
}else
{
this.info.set("zhangsan","www.zhangsan.com");
flag=true;
}
}
}
};
消费者的修改同理,不再给出。
运行部分结果:
张三-->教师
zhangsan-->www.zhangsan.com
张三-->教师
张三-->教师
zhangsan-->www.zhangsan.com
zhangsan-->www.zhangsan.com
zhangsan-->www.zhangsan.com
.....
从上面的输出结果可以看出信息错乱的问题已经得到解决,但是依然存在重复读取的问题,此时就可以使用Object类来帮忙了。
- Object类对线程的支持——等待与唤醒
从前面的学习中知道Object类是所有类的父类,在此类中有以下几种方法是对线程操作有所支持的。
方法 | 类型 | 描述 |
---|---|---|
public final void wait() throws InterruptedException | 普通 | 线程等待 |
public final void wait(long timeout) throws InterruptedException | 普通 | 线程等待,并指定等待的最大时间,毫秒 |
public final void wait(long timeout,int nanos) throws InterruptedException | 普通 | 线程等待,并指定等待的最长毫秒和纳秒 |
public final void notify() | 普通 | 唤醒一个等待的线程 |
public final void notifyAll() | 普通 | 唤醒全部等待的线程 |
从表中可以看出,可以将一个线程设置为等待状态,但是对于唤醒的操作却由notify()和notifyAll()两个方法。一般来说,所有等待的线程依照顺序进行排序,如果现在使用了notify()方法,则会唤醒第一个等待的线程执行,而如果使用了notifyAll()方法,哪个线程的优先级越高,哪个线程就有可能先执行。
- 问题解决2——加入等待与唤醒
如果想让生产者不重复生产,消费者不重复取走,则可以增加一个标志位,假设标志位为boolean型变量,如果标志位的内容为true,则表示可以生产,但是不能取走,此时线程执行到了消费者线程则应该等待,如果标志位的内容为false,则表示可以取走,但是不能生产,如果生产者线程运行,则消费者线程应该等待。
要完成上述功能,直接在Info类中进行修改就行。在Info类中加入标志位,并通过判断标志位来完成等待与唤醒的操作。
【修改Info类】
class Info
{
private String name = "张三";
private String content= "教师";
private boolean flag=false;
public synchronized void set(String name,String content){
if (!flag)
{
try
{
super.wait();
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
this.setName(name);
try
{
Thread.sleep(300);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
this.setContent(content);
flag=false;
super.notify();
}
public synchronized void get(){
if (flag)
{
try
{
super.wait();
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
try
{
Thread.sleep(300);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
System.out.println(this.getName()+"-->"+this.getContent());
flag =true;//可以进行生产
super.notify();
}
public String getName(){
return name;
}
public void setName(String name){
this.name=name;
}
public String getContent(){
return content;
}
public void setContent(String content){
this.content=content;
}
};
运行效果:
张三-->教师
zhangsan-->www.zhangsan.com
张三-->教师
zhangsan-->www.zhangsan.com
张三-->教师
zhangsan-->www.zhangsan.com
张三-->教师
。。。。。
从程序运行的效果来看。一个生产者生产完成,就等待一个消费者取出,消费者取出一个就等待生产者进行生产,这样就避免了重复生产和重复取出的问题