同属于一个进程的多个线程,是共享地址空间的,它们可以一起协作来完成指定的任务。因此,线程之间必须相互通信,才能完成协作。
一、引入问题
下面通过一个应用案例来讲解线程间的通信。把一个数据储存空间划分为两个部分:一部分用于储存用户的姓名,另一部分用于储存用户的性别。
这个案例包含两个线程:一个线程向数据存储空间添加数据(生产者),另一个线程从数据存储空间中取出数据(消费者)。这个程序有两种意外需要考虑:
第一种意外,假设生产者线程刚向数据储存空间中添加了一个人的姓名,还没有加入这个人的性别,CPU就切换到了消费者线程,消费者线程则把这个人的姓名和上一个人的性别联系到一起。这个过程可用下图表示:
第二种意外,生产者放入了若干次数据,消费者才开始取数据,或者是,消费者取完一个数据后,还没等到生产者放入新的数据,又重新取出已取过的数据。
在操作系统里,上面的案例属于经典的同步问题——生产者消费者问题,下面我们通过线程间的通信来解决上面提到的意外:
二、解决问题
下面先来构思这个程序,程序中的生产者线程和消费者线程运行的是不同的程序代码,因此这里需要编写两个包含有run方法的类来完成这两个线程,一个是生产者类Producer,另一个是消费者类Consumer。
01 class Producer implements Runnable
02 {
03 public void run()
04 {
05 while(true)
06 {
07 //编写往数据存储空间中放入数据的代码
08 }
09 }
10 }
下面是消费者线程的代码:
01 class Consumer implements Runnable
02 {
03 public void run()
04 {
05 while(true)
06 {
07 //编写从数据存储空间中读取数据的代码
08 }
09 }
10 }
当程序写到这里,还需要定义一个新的数据结构Person,用来作为数据储存空间。 在这个数据结构中,类Person只有数据,而没有对数据的操作,非常类似于C语言的结构体。
01 class Person
02 {
03 String name;
04 String sex;
05 }
Producer和Consumer线程中的run()方法都需要操作类Person的同一对象实例。
接下来,对Producer和Consumer这两个类做如下修改,顺便写出程序的主调用类ThreadCommunation:
package com.xy.thread;
class Person {
String name = "小四";
String sex = "女";
}
class Producer implements Runnable {
Person p = null;
public Producer(Person p) {
this.p = p;
}
public void run() {
for(int i = 0; i < 10; i++) {
if(i%2 == 0) {
p.name = "小三";
try {
Thread.sleep(1000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
p.sex = "男";
}
else {
p.name = "小四";
try {
Thread.sleep(1000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
p.sex = "女";
}
}
}
}
class Consumer implements Runnable {
Person q = null;
public Consumer(Person q) {
this.q = q;
}
public void run() {
for(int i = 0; i < 10; i++) {
System.out.println(q.name + "---->" + q.sex);
try {
Thread.sleep(1000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadCommunation {
public static void main(String[] args) {
Person pp = new Person();
new Thread(new Producer(pp)).start();
new Thread(new Consumer(pp)).start();
}
}
【结果】
从输出结果可以看到,原本“小四是女”、“小三是男”,现在却打印了“小四是男”、“小三是女”的奇怪现象。
从程序中可以看到,Producer类和Consumer类都是操纵了一个Person类,这就有可能Producer类还未操纵完P类,Consumer类就已经将P类中的内容取走了,这就是资源不同步的原因。
程序为了模拟生产者和消费者的生产(消费)耗时,分别使用了sleep(1000)方法做了模拟。为了避免这类“生产者没有生产完,消费者就来消费”或“消费者没有消费完,生产者又来生产,覆盖了还没有来得生产及消费的数据”情况,我们在Person类中添加两个同步方法,put() 和get(),这两个方法都使用了synchronized关键词,从而保证了生产或消费操作过程的原子性——即正在生产过程中,不能消费,或消费过程中,不能生产。
具体代码如下范例所示:(仅改变了Person、Producer、Consumer)
class Person {
String name = "小四";
String sex = "女";
public synchronized void set(String name, String sex) {
this.name = name;
this.sex = sex;
}
public synchronized void get() {
System.out.println(this.name + "---->" + this.sex);
}
}
class Producer implements Runnable {
Person p = null;
public Producer(Person p) {
this.p = p;
}
public void run() {
for(int i = 0; i < 10; i++) {
if(i%2 == 0) {
p.set("小三", "男");
}
else {
p.set("小四", "女");
}
}
}
}
class Consumer implements Runnable {
Person q = null;
public Consumer(Person q) {
this.q = q;
}
public void run() {
for(int i = 0; i < 10; i++) {
q.get();
}
}
}
【结果】
可以看到程序的输出结果是正确的,能够保证“李四是女的”。但是另外一个问题又产生了,从程序的执行结果来看,Consumer线程对Producer线程放入的一次数据连续地读取了多次,多次输出:“李四 ---->女”,这并不符合实际的要求。
合理的结果应该是,Producer放一次数据,Consumer就取一次;反之,Producer也必须等到Consumer取完后才能放入新的数据,而这一问题的解决就需要使用线程间的通信。
三、线程间的通信
Java是通过Object类的wait()、notify ()、notifyAll ()这几个方法来实现线程间的通信的,又因为所有的类都是从Object继承的,因此任何类都可以直接使用这些方法。
下面是这3个方法的简要说明:
wait():通知当前线程进入睡眠状态,直到其他线程进入并调用notify()或notifyAll()为止.在当前线程睡眠之前,该线程会释放所占有的“锁标志”,即其占有的所有synchronized标识的代码块可被其他线程使用。
notify():唤醒在该同步代码块中第1个调用wait()的线程。
这类似排队买票,一个人买完之后,后面的人才可以继续买。
notifyAll():唤醒该同步代码块中所有调用wait的所有线程,具有最高优先级的线程首先被唤醒并执行。
如果想让上面的程序符合预先的设计需求,就必须在类Person中定义一个新的成员变量bFull来表示数据储存空间的状态。当Consumer线程取走数据后,bFull值为false,当Producer线程放入数据后,bFull值为true。只有bFull为true时,Consumer线程才能取走数据,否则就必须等待Producer线程放入新的数据后的通知;反之,只有bFull为false,Producer线程才能放入新的数据,否则就必须等待Consumer线程取走数据后的通知。修改后的P类的程序代码如下:
package com.xy.thread;
class Person {
String name = "小四";
String sex = "女";
private boolean bFull = false;
public synchronized void set(String name, String sex) {
if(bFull) {
try {
wait(); // 后来的线程要等待
}
catch(InterruptedException e) {
e.printStackTrace();
}
}
this.name = name;
this.sex = sex;
bFull = true;
notify(); // 唤醒最先到达的线程
}
public synchronized void get() {
if(!bFull) {
try {
wait();
}
catch(InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(this.name + "---->" + this.sex);
bFull = false;
notify();
}
}
class Producer implements Runnable {
Person p = null;
public Producer(Person p) {
this.p = p;
}
public void run() {
for(int i = 0; i < 10; i++) {
if(i%2 == 0) {
p.set("小三", "男");
}
else {
p.set("小四", "女");
}
}
}
}
class Consumer implements Runnable {
Person q = null;
public Consumer(Person q) {
this.q = q;
}
public void run() {
for(int i = 0; i < 10; i++) {
q.get();
}
}
}
public class ThreadCommunation {
public static void main(String[] args) {
Person pp = new Person();
new Thread(new Producer(pp)).start();
new Thread(new Consumer(pp)).start();
}
}
【结果】
需要注意的是,wait()、notify()、notifyAll()这3个方法只能在synchronized方法中调用,即无论线程调用的是wait()还是notify()方法,该线程必须先得到该对象的所有权。这样,notify()就只能唤醒同一对象监视器中调用wait()的线程。而使用多个对象监视器,就可以分别有多个wait()、notify()的情况,同组里的wait()只能被同组的notify()唤醒。