多线程(四) —— 生产者和消费者
目录
加深线程同步操作的理解
在线程操作中有一个经典的案例程序 —— 生产者和消费者问题,生产者不断生产,消费者不断取走生产者生产的产品。
第一种信息:
“张冬晖”、“Java讲师”
第二种信息:
“dong”、“com.dong1990”
在图中非常清楚的表示出,生产者生产出信息之后将其放到一个区域之中,之后消费者从此区域里取出数据
程序的问题:
但是在本程序中因为牵涉到线程运行的不确定性,所以会存在以下两点问题:
- 假设生产者线程刚向数据存储空间添加了信息的名称,还没有加入这信息的内容,程序就切换到了消费者线程,消费者线程将把这信息的名称和上一个信息的内容联系到了一起。
- 生产者放了若干次的数据,消费者才开始取数据,或者是,消费者取完一个数据后,还没等到生产者放入新的数据,又重复取出已取过的数据。
下面分别来看这两个问题的产生。
程序的基本实现
因为现在程序中生产者不断生产的是信息,而消费者不断取出的也是信息,所以定义一个保存信息的类 —— Info.java。
class Info { // 定义信息类
private String name = "张冬晖"; // 信息名称,指定默认值
private String content = "JAVA讲师"; // 信息内容,指定默认值
public String getName() { // 取得信息名称
return name; // 返回信息名称
}
public void setName(String name) { // 设置信息名称
this.name = name; // 设置name属性内容
}
public String getContent() { // 取得信息内容
return content; // 返回信息内容
}
public void setContent(String content) { // 设置信息内容
this.content = content; // 设置content属性内容
}
}
生产者
class Producer implements Runnable { // 定义生产者线程
private Info info = null; // 保存Info引用
public Producer(Info info) { // 通过构造方法设置Info属性内容
this.info = info; // 为info属性初始化
}
public void run() { // 覆写run()方法
boolean flag = false; // 定义标记位
for (int i = 0; i < 50; i++) { // 循环50次
if (flag) { // 如果为true,则设置第一个信息
this.info.setName("张冬晖"); // 设置信息名称
Thread.sleep(90); // 加入延迟,处理异常
this.info.setContent("JAVA讲师"); // 设置信息内容
flag = false; // 修改标记位
} else { // 如果为false,则设置第二个信息
this.info.setName("dong"); // 设置信息名称
Thread.sleep(90); // 加入延迟,处理异常
this.info.setContent("com.dong1990"); // 设置信息内容
flag = true; // 修改标记位
}
}
}
}
消费者
class Consumer implements Runnable { // 定义消费者线程
private Info info = null; // 保存Info引用
public Consumer(Info info) { // 通过构造方法设置Info属性内容
this.info = info; // 为info属性初始化
}
public void run() { // 覆写run()方法
for (int i = 0; i < 50; i++) { // 循环50次
try {
Thread.sleep(100); // 加入延迟
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.info.getName() + " --> "
+ this.info.getContent());// 取出信息
}
}
}
测试程序
public class ThreadCaseDemo01 {
public static void main(String[] args) {
Info i = new Info(); // 实例化Info对象
Producer pro = new Producer(i); // 实例化生产者,传递Info引用
Consumer con = new Consumer(i); // 实例化消费者,传递Info引用
new Thread(pro).start(); // 启动生产者线程
new Thread(con).start(); // 启动消费者线程
}
}
问题解决1 —— 加入同步
如果要想为操作加入同步,则可以通过定义同步方法的方式完成,即:将设置名称和姓名定义成一个同步方法完成。
修改Info类
class Info { // 定义信息类
private String name = "张冬晖"; // 信息名称,指定默认值
private String content = "JAVA讲师"; // 信息内容,指定默认值
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()); // 输出信息
}
// setter及getter方法
}
修改生产者
class Producer implements Runnable { // 定义生产者线程
private Info info = null; // 保存Info引用
public Producer(Info info) { // 通过构造方法设置Info属性内容
this.info = info; // 为info属性初始化
}
public void run() { // 覆写run()方法
boolean flag = false; // 定义标记位
for (int i = 0; i < 50; i++) { // 循环50次
if (flag) { // 如果为true,则设置第一个信息
this.info.set("张冬晖", "JAVA讲师"); // 设置信息
flag = false; // 修改标记位
} else { // 如果为false,则设置第二个信息
this.info.set("dong", "com.dong1990"); // 设置信息
flag = true; // 修改标记位
}
}
}
}
修改消费者
class Consumer implements Runnable { // 定义消费者线程
private Info info = null; // 保存Info引用
public Consumer(Info info) { // 通过构造方法设置Info属性内容
this.info = info; // 为info属性初始化
}
public void run() { // 覆写run()方法
for (int i = 0; i < 50; i++) { // 循环50次
try {
Thread.sleep(100); // 加入延迟
} catch (InterruptedException e) {
e.printStackTrace();
}
this.info.get(); // 取出信息
}
}
}
问题的解决
了解Object类中对线程的支持方法
—— 等待与唤醒
Object类是所有类的父类,在此类中有以下几个方法是对线程操作有所支持的。
No. |
方法 |
类型 |
描述 |
1 |
public final void wait() throws InterruptedException |
普通 |
线程等待 |
2 |
public final void wait(long timeout) throws InterruptedException |
普通 |
线程等待,并指定等待的最长时间,以毫秒为单位 |
3 |
public final void wait(long timeout,int nanos) throws InterruptedException |
普通 |
线程等待,并指定等待的最长毫秒及纳秒 |
4 |
public final void notify() |
普通 |
唤醒第一个等待的线程 |
5 |
public final void notifyAll() |
普通 |
唤醒全部等待的线程 |
notify()和notifyAll()
对于唤醒的操作有两个:notify()、notifyAll()。一般来说,所有等待的线程会按照顺序进行排列,如果现在使用了notify()方法的话,则会唤醒第一个等待的线程执行,而如果使用了notifyAll()方法,则会唤醒所有的等待线程,那个线程的优先级高,那个线程就有可能先执行。
问题解决2 —— 加入等待与唤醒
如果要想让生产者不重复生产,消费者不重复取走,则可以增加一个标志位,假设标志位为boolean型变量,如果标志位的内容为true,则表示可以生产,但是不能取走,如果此时线程执行到了消费者线程则应该等待,如果标志位的内容为false,则表示可以取走,但是不能生产,如果生产者线程运行,则应该等待。
修改Info类
class Info { // 定义信息类
private String name = "张冬晖"; // 信息名称,指定默认值
private String content = "JAVA讲师"; // 信息内容,指定默认值
private boolean flag = false ; // 设置标志位
public synchronized void set(String name, String content) { // 设置信息名称及内容
if(!flag){ // 标志位为false,不可以生产
super.wait() ; // 等待消费者取走,处理异常 }
this.setName(name); // 设置信息名称
Thread.sleep(300); // 加入延迟,处理异常
this.setContent(content) ; // 设置信息内容
flag = false ; // 修改标志位,表示可以取走
super.notify() ; // 唤醒等待线程
}
public synchronized void get() { // 取得信息内容
if(flag){ // 标志位为true,不可以取走
super.wait() ; // 等待生产者生产,处理异常
}
Thread.sleep(300); // 加入延迟,处理异常
System.out.println(this.getName()
+ " --> " + this.getContent()); // 输出信息
flag = true ; // 修改标志位为true,表示可以生产
super.notify() ; // 唤醒等待线程
}
// setter()及getter()方法
}
上一篇: Android基础学习总结(十二)——利用Bmob实现用户账户体系(注册、登录、验证等)
下一篇: 基于mysql中文乱码的“一万种”可能,彻底告别中文乱码(欢迎补充) 博客分类: 数据库db 中文乱码问号?mysqljsp
推荐阅读
-
多线程(四) —— 生产者和消费者
-
线程同步模型, 生产者/消费者, 读写同步,线程池,concurrent map. 博客分类: Design 多线程thread数据结构log4j编程
-
线程同步之条件变量 生产者和消费者模型
-
Java多线程-生产者于消费者 博客分类: javajust do itmore and more java多线程thread生产者和消费者经典
-
java多线程解决生产者消费者问题
-
java多线程解决生产者消费者问题
-
java 中多线程生产者消费者问题详细介绍
-
java 中多线程生产者消费者问题详细介绍
-
Java生产者和消费者例子_动力节点Java学院整理
-
Java多线程之线程通信生产者消费者模式及等待唤醒机制代码详解