java线程(四)——线程协作

 一.执行顺序问题

假如有一个Watch类,它有两个功能,一个是显示时间,一个是记录时间,所以其中有两个内部类Display和Time。这两个内部类都是独立运行的线程类,都重写了run方法,一个获取当前时间到全局变量time中,一个将time中的当前时间打印。代码如下

package tea;

import java.util.Date;

public class Watch {

	private static String time;
	
	static class Display extends Thread{

		@Override
		public void run() {
			System.out.println(time);
		}
	}
	
	static class Time extends Thread{

		@Override
		public void run() {
			time=new Date().toString();
		}
	}
	
	public static void main(String[] args) {
		new Time().start();
		new Display().start();
	}
}

那么问题来了,在主线程中同时启动两个内部类线程,由于线程的执行是抢占式的,所以这时假如Display线程先抢到了执行权,此时全局变量time中仍然是默认值null,所以打印结果会是null,没有达到目的。况且由于获取当前时间的语句的复杂度要高于输出语句,所以大概率会打印null:

这便是多线程中常常出现的问题,就是线程之间的抢占式执行会导致执行顺序不可控,下面来介绍几种控制线程执行顺序的方法。

二.使用join方法

join方法的作用就是将执行该行代码的线程变为阻塞状态,将调用该方法的线程变为运行状态,直到调用该方法的线程运行结束。

使用的代码示例如下:

类加载第7行:创建静态全局变量time。

执行到第37、38行:创建Time类线程对象并开启线程。

执行到第39行:创建Display类线程对象并向Display线程对象中传入time线程对象,开启线程。

这时两个内部类线程已开启,开始抢占式执行,假设这时Display线程对象先抢到了执行权:

执行到第13行:在创建Display线程对象时传入了time线程对象,所以该构造方法将传入的time线程对象赋值给了Display类中的变量timeThread。

执行到第17行:判断全局变量time是否为null,也就是判断time线程是否执行过,因为假设Display线程先抢到的执行权,所以此时time线程还没执行,所以全局变量time值为null,条件成立,执行下面代码块。

执行到第19行:Display线程执行该行代码,所以Display线程进入阻塞状态,因为timeThread变量中存储的是time线程对象的地址,所以是time线程在调用join方法,所以time线程进入运行状态。

执行到第32行:全局变量time获取当前时间,time线程结束。

执行到第24行:将全局变量中的当前时间打印,Display线程结束。

这便达到了让Time和display两个线程按顺序执行。

package tea;

import java.util.Date;

public class Watch {

	private static String time;
	
	static class Display extends Thread{

		Time timeThread;
		public Display(Time time) {
			this.timeThread=time;
		}
		@Override
		public void run() {
			if(time==null) {
				try {
					timeThread.join();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			System.out.println(time);
		}
	}
	
	static class Time extends Thread{

		@Override
		public void run() {
			time=new Date().toString();
		}
	}
	
	public static void main(String[] args) {
		Time time=new Time();
		time.start();
		new Display(time).start();
	}
}

三.wait()、notify()和notifyAll()协作

1.使用示例

在博客:原 java线程(三)——线程中的数据共享中说到,使用synchronized关键字可以解决并发执行的问题,但是哪个线程先执行,哪个线程后执行依无法确定,而Object类中的wait()、notify()和notifyAll()三个方法解决了线程间的协作问题,通过这三个方法搭配上synchronized关键字,“合理”使用可以确定多线程中线程的先后执行顺序,使用示例如下:

大体上与使用join方法区别不大。

第53行:创建一个Object类常量对象OBJECT。

第12到16行:在Display线程类的构造方法中传入线程的名字和常量对象OBJECT,并赋值给线程类中的object变量。

第34到37行:在Time线程类的构造方法中传入常量对象OBJECT,赋值给线程类中的object变量。这时Display和Time两个线程类中就有共享数据OBJECT了。

第20到26行:此时如果判断全局变量time中为null,说明Time线程还未执行,此时便执行第22的代码,用共享数据object调用wait方法,其作用是让执行这行代码的线程,也就是Display线程进入等待状态,然后执行权来到Time线程。

第40到45行:在五秒的等待过后,获取当前时间到全局变量time中。

第47行:用共享数据object调用notify方法,该方法的作用就是相当于发出一个信号,解除该共享数据在其他线程中处于的等待状态(wait),解除过后Time线程结束执行,所以此时Display线程又从等待状态变为就绪状态了。

第28行:打印全局变量time中的当前时间,线程结束执行。

使用该方法达到控制执行顺序的原理是使用wait让后执行的线程处于等待状态。

package tea;

import java.util.Date;

public class Watch {

	private static String time;
	
	static class Display extends Thread{

		Time timeThread;
		Object object;
		public Display(Time time,Object object) {
			this.timeThread=time;
			this.object=object;
		}
		@Override
		public void run() {
			if(time==null) {
				synchronized (object) {
					try {
						object.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
			System.out.println(time);
		}
	}
	
	static class Time extends Thread{

		Object object;
		public Time(Object object) {
			this.object=object;
		}
		@Override
		public void run() {
			try {
				sleep(5000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			time=new Date().toString();
			synchronized (object) {
				object.notify();
			}
		}
	}
	
	public static void main(String[] args) {
		final Object OBJECT = new Object();
		Time time=new Time(OBJECT);
		time.start();
		new Display(time,OBJECT).start();
	}
}

2.wait()和sleep()区别

sleep()方法被调用后当前线程进入阻塞状态,但是当前线程仍然持有对象锁,在当前线程sleep期间,其它线程无法执行sleep方法所在临界区中的代码。而wait方法在等待时会释放对对象锁的控制权,可以允许其他共享数据的线程进入对象锁。

例如如下代码:

尽管此处是死循环,但由于对象锁lockObj调用了wait()方法,使得分别持有该lockObj对象锁的“1号计数器”线程和“2号计数器”线程处于线程等待状态,所以循环并没有继续下去

class CounterThread extends Thread {

	private Object lockObj;

	public CounterThread(String threadName, Object lockObj) {
		super(threadName);
		this.lockObj = lockObj;
	}

	@Override
	public void run() {
		int i = 1;
		while (true) {
			synchronized (lockObj) {
				System.out.println(getName() + ":" + i);
				try {
					lockObj.wait();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				i++;
			}
		}
	}
}

public class Test {

	public static void main(String[] args) {
		Object lockObj = new Object();
		new CounterThread("1号计数器",lockObj).start();
		new CounterThread("2号计数器",lockObj).start();
	}
}

3.notify()和notifyAll()

notify方法可以解除共享数据的线程中处于等待状态的线程,但是它只能解除其中一个线程的等待状态,也就是说当有多个线程处于等待状态时,notify只会随机从中解除一个线程的等待状态。

而notifyAll方法则可以解除当前共享数据的线程中所有处于等待状态的线程,使用示例如下:

在开启的两个CounterThread线程中,都有一个死循环,但是每执行一次循环都会陷入wait的等待状态,而在主线程sleep五秒阻塞之后,开启了NotifyAll线程,在该线程中用共享的数据调用了notifAll方法解除了该共享数据的所有的处于等待状态下的线程,所以这时两个CounterThread线程又执行了一遍while循环然后又处于wait等待状态,这次没有notify来帮它们解除等待状态了,所以线程便阻塞在这里无法继续执行了,运行结果如下:

class CounterThread extends Thread {

	private Object lockObj;

	public CounterThread(String threadName, Object lockObj) {
		super(threadName);
		this.lockObj = lockObj;
	}

	@Override
	public void run() {
		int i = 1;
		while (true) {
			synchronized (lockObj) {
				System.out.println(getName() + ":" + i);
				try {
					lockObj.wait();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				i++;
			}
		}
	}
}

class NotifyAllThread extends Thread {

	private Object lockObj;

	public NotifyAllThread(Object lockObj) {
		this.lockObj = lockObj;
	}

	@Override
	public void run() {
		synchronized (lockObj) {
			System.out.println("notifyAll方法执行完毕");
			lockObj.notifyAll();
		}
	}
}

public class Test {

	public static void main(String[] args) {
		Object lockObj = new Object();
		new CounterThread("1号计数器", lockObj).start();
		new CounterThread("2号计数器", lockObj).start();
		
		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		new NotifyAllThread(lockObj).start();
	}
}

4.注意事项

①wait()方法需要和notify()或notifyAll()方法中的一个配对使用,且wait方法与notify()或notifyAll()方法配对使用时不能在同一个线程中,因为在同一个线程中一旦执行到了wait,线程便处于阻塞状态了,无法再继续往下执行了,所以即使下面有notify或notifyAll方法,也执行不到,没法解除wait状态。

②wait()方法、notify()方法和notifyAll()方法必须在同步方法或者同步代码块中使用,否则出现IllegalMonitorStateException 异常

假如将上面的那些示例中的任意一个synchronized包裹去掉,便会报出如下异常:

③调用wait()方法、notify()方法和notifyAll()方法的对象必须和同步锁对象是一个对象。

发布了99 篇原创文章 · 获赞 93 · 访问量 5238

猜你喜欢

转载自blog.csdn.net/DangerousMc/article/details/100055278