在线程操作中有一个经典的案例,即生产者和消费者问题。生产者不断生产,消费者不断取走生产者生产的产品。即生产者生产出的产品放到一个区域中,消费者从该区域中取走产品.如下图所示。
在上面的方式中,如果使用多线程运行,由于多线程运行的不确定性,会存在以下问题:
- 假设生产者线程刚向数据存储空间中加入该信息的名称,还没有加入信息的内容,程序就切换到了消费者线程,消费者线程把信息的名称和上一个信息的内容联系到一起。
- 生产者放入若干次数的数据,消费者才开始取出数据,
或者是,消费者取完了一个数据后,还没等到生产者放入新的数据,又重复放入以取过的数据。
生产者消费者的基本实现
因为现在程序中生产者不断生产的是信息,而消费者不断取出的也是信息,所以这定义一个保存信息的类: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的内容
从运行结果中看,出现了下面两个问题:
- 取出的产品名称为
产品A
,但是内容却是产品B的内容
,这不符合要求,这是取出数据不匹配的问题。 本次取出的是产品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中线程相关的方法 | 描述 |
---|---|---|
1 | void wait() | 当前线程等待。 |
2 | void wait(long timeout) | 当前线程等待。并指定等待的时间(毫秒) |
3 | void wait(long timeout, int nanos) | 当前线程等待,并指定等待的毫秒,以及纳秒 |
4 | void notify() | 唤醒在此对象监视器上等待的单个线程。 |
5 | void notifyAll() | 唤醒在此对象监视器上等待的所有线程。 |
从上面的表中可以看出,可以将一个线程设置为等待状态,但是对于唤醒的操作却又两个,分别为notify()和notifyAll()。一般来说,所有等待的线程会按照顺序进行排列,如果此时使用了notify()方法,则会唤醒第一个等待的线程,第一个等待的线程得以执行,而如果使用了notifyAll()方法,则会唤醒所有的等待的线程,哪个线程优先级高,哪个线程就有可能先得到CPU调度而先执行。
解决生产者消费者重复问题—加入等待与唤醒机制
如果想让生产者不重复生产,消费者不重复取走,则可以增加一标准位,设置标志位为boolean变量,如果标着为的内容为true,这表示可以生产,但是不能取走,此时若执行到了消费者线程,则消费者线程应该等待。如果标志位的内容为false,则表示可以取走,但是此时不能生产,如果执行到了生产者线程,则生产者线程应该等待。操作流程如下所示。
要完成以上的功能,直接在 上面同步方法的例子的基础上修改一下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的内容