Java之路:线程间的通信

本文通过一个应用案例介绍了Java中线程间通信的重要性,详细讲解了如何解决生产者消费者问题。通过使用`synchronized`关键字保证操作原子性,并通过`wait()`、`notify()`方法实现线程间的通信,确保数据的正确处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

同属于一个进程的多个线程,是共享地址空间的,它们可以一起协作来完成指定的任务。因此,线程之间必须相互通信,才能完成协作。

一、引入问题

下面通过一个应用案例来讲解线程间的通信。把一个数据储存空间划分为两个部分:一部分用于储存用户的姓名,另一部分用于储存用户的性别。

这个案例包含两个线程:一个线程向数据存储空间添加数据(生产者),另一个线程从数据存储空间中取出数据(消费者)。这个程序有两种意外需要考虑:

第一种意外,假设生产者线程刚向数据储存空间中添加了一个人的姓名,还没有加入这个人的性别,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()唤醒。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值