【Java多线程】——线程间通信(1)

    进行多线程编程时,每个线程都是操作系统中的独立个体,但线程在执行任务的时候并不是只关注于完成自己的那一部分任务,很多时候需要与其他线程协作或者通过和其他线程的通信完成相应的任务。这个时候我们便要引入线程间通信的这个概念了。而在多线程编程中最常用到的线程间通信方法就是等待/通知机制了。在介绍等待/通知机制之前,我们先来看一个最简单的线程间通信的方式。

一、不使用等待/通知

    我们可以使用sleep()结合while(true)死循环结合的方法来实现多个线程之间的通信。

    如下代码,创建两个线程类ThreadA和ThreadB,其中第一个线程用来执行一个循环10次的操作,依次向一个集合中添加元素;第二个线程在集合中有五个元素的时候,打印语句“B任务执行!”。

public class ThA extends Thread{
	private List ls;
	public ThA(List ls) {
		super();
		this.ls = ls;
	}
	@Override
	public void run() {
		try {
			for(int i=0;i<10;i++) {
				ls.add("OceaNier");
				System.out.println("添加了"+(i+1)+"个元素。"+Thread.currentThread().getName());
	            Thread.sleep(1000);
			}
		}catch(InterruptedException e) {
			e.printStackTrace();
		}
	}
}

public class ThB extends Thread{
	private List ls;
	public ThB(List ls) {
		super();
		this.ls = ls;
	}
	@Override
	public void run() {
		try {
			while(true) {
				if(ls.size() == 5) {
					System.out.println("123123");
					Thread.sleep(1000);
				}
			}
		}catch(InterruptedException e) {
			e.printStackTrace();
		}
	}
}

依次运行上述两个线程,可以看到如下运行结果:

/*

添加了4个元素。A

添加了5个元素。A

123123

添加了6个元素。A

添加了7个元素。A

*/

    虽然上述程序实现了线程间的通信功能,但是我们可以发现,线程A在运行的过程中,线程B是通过不断查看ls.size()的值来判断自己什么时候该执行输出语句。换句话说,这种轮询的方式存在一些弊端,首先会浪费许多CPU资源,CPU需要分出一部分时间便提供给线程B执行判断语句,而如果轮询频率较高则会浪费更多的cpu时间,轮询时间间隔过长又容易遗漏关键信息;其次这种线程间通信的实现方式并不是真正的通信,而是对于线程A来说的一种被动通信行为。好比服务员和厨师之间,如果厨师在做菜,服务员每隔一分钟进厨房观察是否做好指定的菜,如果没有则退出厨房,显然这是效率极低的一种方式。我们想要的是,当服务员需要厨师做菜的时候可以告诉厨师,而当厨师做好相应的菜的时候可以提示服务员。

二、等待/通知机制

    要解决上面的问题,多线程编程中一般使用等待/通知机制,也就是wait()/notify()机制。

    首先我们来解释一下wait(),wait的作用是使当前线程进入阻塞状态,必须在当前线程池有对象锁的时候使用,也就是说必须在同步代码块的内部使用wait,如果使用wait时程序没有持有相应对象的对象锁,则会抛出IllegalMonitorStateException异常,此异常属于RuntimeException,不需要做catch捕捉处理。在执行wait方法之后,当前线程将会被阻塞,进入预执行队列,等待被唤醒重新竞争对象锁。

    方法notify()和notifyAll()的作用相似,notify()也是需要在同步代码块内执行的,也就是说执行notify方法的时候,程序必须持有相应对象的对象锁。在notify()执行之后,系统会唤醒一个处于wait状态的线程,使其进入就绪状态,具备对于锁的竞争能力。而调用notify()的线程不会立马释放对象锁,它会等待当前的同步代码块执行完毕之后再释放锁。而notifyAll的作用就是唤醒所有的等待此对象锁的线程,让他们竞争对象锁,竞争到对象锁的线程会开始继续执行wait方法后面的代码,而没有竞争到对象锁的线程继续进入等待状态,等待下一次唤醒。如果notifyAll()执行后没有发现处于阻塞状态中的线程,则这一次唤醒被忽略。

    将前面的程序更改为等待/通知机制后如下:

public class ThA extends Thread{
	private Object lock;
	private List ls;
	public ThA(List ls,Object lock) {
		super();
		this.ls = ls;
		this.lock = lock;
	}
	@Override
	public void run() {
			synchronized(lock) {
				for(int i=0;i<10;i++) {
					if(ls.size()==5) {
						lock.notifyAll();
						System.out.println("已通知唤醒");
					}
					ls.add("OceaNier");
					System.out.println("添加了"+(i+1)+"个元素。");
				}
			}
	}
}
public class ThB extends Thread{
	private Object lock;
	private List ls;
	public ThB(List ls,Object lock) {
		super();
		this.ls = ls;
		this.lock = lock;
	}
	@Override
	public void run() {
		try {
			synchronized(lock) {
				lock.wait();
				System.out.println("线程B的输出!");
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

运行结果如下:

/*

添加了1个元素。

添加了2个元素。

添加了3个元素。

添加了4个元素。

添加了5个元素。

已通知唤醒

添加了6个元素。

添加了7个元素。

添加了8个元素。

添加了9个元素。

添加了10个元素。

线程B的输出!

*/

    从上面的程序中我们可以看到两个结论:等待/通知机制使得需要等待的线程主动放弃对象锁转入阻塞状态,在阻塞期间不占用任何处理机资源;唤醒线程在适当的时间将阻塞线程唤醒,但是并不立即释放对象锁,所以可以看到“线程B的输出”直到线程A最后执行完毕才进行打印。

    ps:当线程属于wait()状态时,调用interrupt()方法会抛出InterruptException异常。

三、线程状态切换总结


1)新创建一个线程后,调用这个线程的start()方法,系统会为此线程分配CPU资源,使其处于Runnable状态,也可以称作就绪状态,意思就是,这个线程已经准备就绪了,拥有了一切需要的资源,只等待CPU分配时间片。如果线程抢占到CPU资源,则进入Running状态;

2)Runnable阶段和Running阶段的线程可以互相切换状态,即就绪状态的线程是具备运行的一切条件的,只等待cpu分配时间片;而处在运行状态的线程则可能在时间片用完之后被更高优先级的线程抢占cpu资源,此时便转化为就绪状态。

    线程进入就绪(Runnable)状态大概有如下几种情况:1.调用sleep()后经过了指定的时间;2.线程调用的阻塞IO已经返回,阻塞方法执行完毕;3.线程成功获得同步监视器资源;4.线程在等待通知的时候被唤醒;5.处于挂起(suspend)状态的线程调用了rensum()方法。

3)Block是阻塞的意思,比如线程在执行的过程中需要进行IO操作,此时cpu就会处于空闲的状态,会浪费cpu资源。这时,使线程进入阻塞状态,让出cpu资源给其他线程使用,等到IO阻塞操作执行完毕,再由Block状态转为Runnable状态,重新竞争cpu资源。

    线程进入Block状态主要有以下几种情况:1.线程调用了sleep()方法,主动放弃处理器资源;2)线程调用了阻塞式IO方法,在阻塞方法返回前线程处于Block状态;3)线程试图获得同步监视器(对象锁),而此同步监视器正在被其他线程占用;4)线程正在等待某个通知;5)程序调用了suspend()方法将线程挂起,不过此方法容易造成死锁,不建议使用。

4)run()方法执行完毕后,进入销毁阶段,整个线程执行完毕。

ps:每个锁对象都有两个队列,一个是就绪队列,另一个是阻塞队列。就绪队列存储了即将要获得锁的线程,阻塞队列存储了被阻塞的线程。一个线程被唤醒后会进入就绪队列,等待cpu分配资源;而一个线程执行wait后,就会放弃锁,进入阻塞队列,等待下一次被唤醒。


猜你喜欢

转载自blog.csdn.net/u012198209/article/details/80283620