9.7 线程操作案例 --- 生产者及消费者
在线程操作中有一个经典的案例程序, 即生产者和消费者问题 ,生产者不断生产;消费者不断取走生产者生产的产品。 生产者生产出信息后将其放到一个区域之中,消费者从此区域中取出数据 ,但是在本程序中因为牵涉到线程运行的不确定性,所以会存在以下两点问题:
- 假设生产者线程刚向数据存储空间添加了信息的名称,还没有加入该信息的内容, 程序就切换到了消费者线程 ;消费者线程将把信息的名称和上一个信息的内容联系到一起。
- 生产者放入了若干次的数据,消费者才开始取数据; 或者是, 消费者取完一个数据后,还没有等到生产者放入新的数据,又重复取出已取出过的数据。
9.7.1 程序的基本实现
因为程序中生产者不断生产的是信息,而消费者不断取出的也是信息, 所以定义一个保存信息的类 Info.java。 代码如下 : 要理解为什么此类不需要构造方法
class Info{
private String name; //private String name = "forfan06";
private int age; //private int age = 27;
/*
-------------------------------
public info(String name, int age){
this.name = name;
this.age = age;
}
-----------------------------
*/
public void setName(String name){
this.name = name;
}
public String getName(){
return this.name;
}
public void setAge(int age){
this.age = age;
}
public int getAge(){
return this.age;
}
}
Info类只包含了保存信息名称的 name属性和age属性,因为生产者和消费者要操作同一个空间的内容,所以生产者和消费者分别实现Runnable接口,并接收Info类的应用。类生产者和消费者的代码如下
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.setName("User" + i);
try{
Thread.sleep(90);
}catch(InterruptedException e){
e.printStackTrace();
}
this.info.setAge(i*2-1);
flag = false;
}else{
this.info.setName("Customer" + i);
try{
Thread.sleep(90);
}catch(InterruptedException e){
e.printStackTrace();
}
this.info.setAge(i*2);
flag = true;
}
}
}
}
在生产者、和消费者的构造方法中传入了Info类的实例化对象,然后在run()方法中循环了50次以产生信息的具体内容。 为了更容易发现以上列出的两个问题,在本程序中设置信息名称和内容的地方加入了延迟操作Threa.sleep();
class Customer implements Runnable{
private Info info = null;
public Customer(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() + ", info: " + this.info.getAge() + "!!!!");
}
}
}
测试程序:
public class ThreadCaseDemo01{
public static void main(String args[]){
Info info = new Info();
Producer p1 = new Producer(info);
Customer c1 = new Customer(info);
new Thread(p1, "Producer").start();
new Thread(c1, "Customer").start();
}
}
运行结果截图:
User41, info: 80!!!!
Customer42, info: 81!!!!
User43, info: 84!!!!
Customer44, info: 85!!!!
User45, info: 88!!!!
...
Customer46, info: 89!!!!
User47, info: 92!!!!
Customer48, info: 93!!!!
User49, info: 96!!!!
User49, info: 97!!!!
9.7.2 问题解决1 --- 加入同步
如果要为操作加入同步,则可以通过定义同步方法的方式完成,即将设置名称和姓名定义成一个方法,代码如下所示:
Info类
class Info{
private String name = "forfan06";
private String content = "WHPU";
public synchronized void set(String name, String content){
this.setName(name);
try{
Thread.sleep(3);
}catch(InterruptedException e){
e.printStackTrace();
}
this.setContent(content);
}
public synchronized void get(){
try{
Thread.sleep(10);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println(this.getName() + "--->" + this.getContent());
}
public String getContent(){
return this.content;
}
public String getName(){
return this.name;
}
public void setName(String name){
this.name = name;
}
public void setContent(String content){
this.content = content;
}
}
以上类中定义了一个set()和get()方法,并且都使用了synchronized关键字进行声明,因为现在不希望直接调用getter()和setter()方法,所以修改生产者和消费者代码如下:
Producer类
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("forfan06", "WHPU");
flag = false;
}else{
this.info.set("Dylan", "CSDN");
flag = true;
}
}
}
}
Customer类
class Customer implements Runnable{
private Info info = null;
public Customer(Info info){
this.info = info;
}
public void run(){
for(int i = 0; i < 50; i++){
try{
Thread.sleep(1);
}catch(InterruptedException e){
e.printStackTrace();
}
this.info.get();
}
}
}
测试类
public class ThreadCaseDemo02{
public static void main(String args[]){
Info info = new Info();
Producer p = new Producer(info);
Customer c = new Customer(info);
new Thread(p, "Set").start();
new Thread(c, "Get").start();
}
}
部分输出结果:
Dylan--->CSDN
forfan06--->WHPU
Dylan--->CSDN
forfan06--->WHPU
forfan06--->WHPU
forfan06--->WHPU
从程序运行的结果可以发现,信息错乱的问题解决了,但是依然存在重复读取的问题,既然有重复读取,肯定会有重复设置的问题,那么对于这样的的问题该如何解决呢??
此时就需要使用Object类。
9.7.3 Object类对线程的支持 --- 等待与唤醒
Object类是所有类的父类,在此类中有以下几种方法时对线程操作有所支持的,如下表所示:
可以将一个线程设置为等待状态,但是对于唤醒的操作有两个,分别为notify()、notifyAll()。 一般来说,所有等待的线程会按顺序进行排列,如果现在使用了notify()方法,则会唤醒第1个等待的线程执行;而如果使用了notifyAll()方法,则会唤醒所有的等待线程,哪个线程的优先级高,哪个线程就有可能先执行。但是还是要看CPU资源被谁先抢到!!
9.7.4 问题解决 2 --- 加入等待与唤醒
如果想让生产者不重复生产,消费者不重复取走,可以增加一个标志位,假设标志位为boolean型变量,如果标志位的内容为true,则表示可以生产,但是不能去走,此时线程执行到消费者线程则应该等待; 如果标志位的内容为false,则表示可以取走,但是不能生产,如果生产者线程运行,则应该等待。 操作流程如下图所示:
要完成以上功能,直接修改Info类即可,在Info类中加入标志位,并通过判断标志位完成等待与唤醒操作。代码如下
Info类
class Info{
private String name = "forfan06";
private String content = "WHPU";
private boolean flag = false;
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;
}
public synchronized void set(String name, String content){
if(!flag){
try{
super.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
this.setName(name);
try{
Thread.sleep(3);
}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(10);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println(this.getName() + "--->" + this.getContent());
flag = true;
super.notify();
}
}
Producer类
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("forfan06", "WHPU");
flag = false;
}else{
this.info.set("Dylan", "CSDN");
flag = true;
}
}
}
}
Customer类
class Customer implements Runnable{
private Info info = null;
public Customer(Info info){
this.info = info;
}
public void run(){
for(int i = 0; i < 50; i++){
try{
Thread.sleep(1);
}catch(InterruptedException e){
e.printStackTrace();
}
this.info.get();
}
}
}
测试类
public class ThreadCaseDemo03{
public static void main(String args[]){
Info info = new Info();
Producer p = new Producer(info);
Customer c = new Customer(info);
new Thread(p, "Set").start();
new Thread(c, "Get").start();
}
}
9.8 线程的生命周期
在java中一个线程对象有自己的生命周期,如果要控制好线程的生命周期,则首先应认识其生命周期,如图所示:
上图中大部分的线程生命周期方法都已经学过,其中的3个新方法介绍如下:
- suspend()方法: 暂时挂起线程
- resume()方法:恢复挂起的线程
- stop()方法:停止线程
**********但是对于线程中suspend()、resume()、stop()3种方法并不推荐使用,因为这3种方法在操作时会产生死锁的问题**********
注意: suspend()、resume()、stop()方法都使用了@Deprecated声明!!!!@Deprecated属于Annotation的语法,表示此操作不建议使用。一旦使用了这些方法将出现警告信息。
既然以上3种方法都不推荐使用那么该如何停止一个线程的执行呢????在多线程的开发中可以通过设置标志位的方法停止一个线程的运行,代码如下:
class MyThread implements Runnable{
private boolean flag = true; //定义标志位属性
public void run(){ //覆写run()方法
int i = 0;
while (this.flag){
while(true){
System.out.println(Thread.currentThread().getName() + "运行, i = " + (i++));
}
}
}
public void stop(){ //编写停止方法
this.flag = false; //修改标志位
}
}
public class StopDemo{
public static void main(String agrs[]){
MyThread my = new MyThread();
Thread t = new Thread(my, "线程");
t.start();
my.stop(); //线程停止,修改标志位
}
}
以上程序一旦调用stop()方法就会将MyThread类中的flag变量设置为false,这样run()方法就会停止运行,这种停止方式是开发中比较常用的。
9.9 本章要点
- 线程(Thread)是指程序的运行流程。多线程机制可以同时运行多个程序块,使程序运行的效率更高,也解决了传统程序设计语言所无法解决的问题。
- 如果要在类中激活线程,必须先做好下面两项准备:(1)此类必须是扩展自Thread类,是自己成为他的子类。(2)线程的处理必须覆写在run()方法内。
- run()方法是定义在Thread类中的一种方法,因此把线程的程序代码编写在run()方法内所做的就是覆写的操作。
- Runnable接口中声明了抽象的run()方法,因此必须在实现Runnable接口的类中明确定义run()方法。
- 在每一个线程创建和消亡之前,均会处于创建、就绪、运行、阻塞、终止状态之一。
- 暂停状态的线程可以由下列情况产生:(1)该线程调用对象的wait()方法时; (2)该线程本身调用Thread的sleep()方法时; (3)该线程和另外一个线程join()在一起时。
- 被冻结因素消失的原因有以下两种情况: (1) 如果线程是由调用对象的wait()方法冻结,则该对象的notify()方法或notifyAll()方法被调用时可以解除冻结。; (2)线程进入休眠sleep()状态,但指定的休眠时间到了。 也会解除冻结。
- 当线程的run()方法运行结束,或是由线程调用其stop()方法时,线程进入消亡状态。
- Thread类中的sleep()方法可以用来控制线程的休眠状态,休眠的时间要视sleep()中的参数而定。
- 要强制某一线程运行,可以join()方法。
- join()方法会抛出InterruptedException异常,所以编写时必须把join()方法编写在try...catch语句块内。
- 当多个线程对象操控同一共享资源时,要使用synchronized关键字来进行资源的同步处理。
9.10 习题
- 设计4个线程对象,两个线程执行减操作,两个线程执行加操作。
- 设计一个生产电脑和搬运电脑类,要求生产出一台电脑就搬走一台电脑,如果没有新的电脑生产出来,则搬运工要等待新电脑产出;如果生产出的电脑没有伴奏,则要等待电脑搬走之后再生产,并统计处生产的电脑数量。
9.11 知识补充:
- notify()、wait()的用法!
- 线程被唤醒后,从何处开始继续执行:是从wait()处? 还是重新从run()开始?
- 可以用this.wait()和this.notify()来控制线程的等待和唤醒么????实验实验!!!
- 线程的唤醒和等待机制