`

Java 多线程同步问题的探究(四、协作,互斥下的协作——Java多线程协作(wait、notify、notifyAll))

阅读更多

Java监视器支持两种线程:互斥和协作

前面我们介绍了采用对象锁和重入锁来实现的互斥。这一篇中,我们来看一看线程的协作。

举个例子:有一家汉堡店举办吃汉堡比赛,决赛时有3个顾客来吃,3个厨师来做,一个服务员负责协调汉堡的数量。为了避免浪费,制作好的汉堡被放进一个能装 有10个汉堡的长条状容器中,按照先进先出的原则取汉堡。如果容器被装满,则厨师停止做汉堡,如果顾客发现容器内的汉堡吃完了,就可以拍响容器上的闹铃, 提醒厨师再做几个汉堡出来。此时服务员过来安抚顾客,让他等待。而一旦厨师的汉堡做出来,就会让服务员通知顾客,汉堡做好了,让顾客继续过来取汉堡。

这里,顾客其实就是我们所说的消费者,而厨师就是生产者。容器是决定厨师行为的监视器,而服务员则负责监视顾客的行为。

在JVM中,此种监视器被称为等待并唤醒监视器

 

在这种监视器中,一个已经持有该监视器的线程 ,可以通过调用 监视对象的wait方法暂停 自身的执行,并释放监视器 ,自己进入一个等待区 ,直到监视器内的 其他线程调用 了监视对象的notify方法 。当一个线程调用唤醒 命令以后,它会持续持有监视器,直到它主动释放监视器。而这之后,等待线程会苏醒,其中的 一个会重新获得监视器,判断条件状态,以便决定是否继续进入等待状态或者执行监视区域,或者退出。

 

package sky.cn.test4;

public class NotifyTest {
	private String flag = "true";
	
	class NotifyThread extends Thread {
		public NotifyThread (String name) {
			super(name);
		}
		
		public void run() {
			try {
				sleep(3000);	//延迟3秒通知
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
			flag = "false";
			flag.notify();
		}
	}
	
	class WaitThread extends Thread {
		public WaitThread(String name) {
			super(name);
		}
		
		public void run() {
			while (flag != "false") {
				System.out.println(getName() + " begin waiting!");
				long startTime = System.currentTimeMillis();
				try {
					flag.wait();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				long endTime = System.currentTimeMillis();
				System.out.println(getName() + " wai time: " + (endTime - startTime));
			}
			System.out.println(getName() + " end waiting!");
		}
	}
	
	public static void main(String[] args) {
		System.out.println("Main Thread Run!");
		NotifyTest test = new NotifyTest();
		NotifyThread notifyThread = test.new NotifyThread("notify01");
		WaitThread waitThread01 = test.new WaitThread("waitThread01");
		WaitThread waitThread02 = test.new WaitThread("waitThread02");
		WaitThread waitThread03 = test.new WaitThread("waitThread03");
		notifyThread.start();
		waitThread01.start();
		waitThread02.start();
		waitThread03.start();
	}
}

 这段代码启动了三个 简单的wait线程 ,当他们处于等待状态以后,试图由一个notify线程 来唤醒。

 

运行这段程序,你会发现,满屏的java.lang.IllegalMonitorStateException ,根本不是你想要的结果。

 

请注意以下几个事实:
   1. 任何一个时刻,对象的控制权(monitor)只能被一个线程拥有。
   2. 无论是执行对象的wait、notify还是notifyAll方法,必须保证当前运行的线程取得了该对象的控制权(monitor)。
   3. 如果在没有控制权的线程里执行对象的以上三种方法,就会报java.lang.IllegalMonitorStateException异常
   4. JVM基于多线程,默认情况下不能保证运行时线程的时序性。

也就是说,当线程在调用某个对象的wait或者notify方法的时候,要先取得该对象的控制权,换句话说,就是进入这个对象的监视器。

 

通过前面对同步的讨论,我们知道,要让一个线程进入某个对象的监视器 ,通常有三种方法

1: 执行对象的某个同步实例方法
2: 执行对象对应的同步静态方法
3: 执行对该对象加同步锁的同步块

显然,在上面的例程中,我们用第三种方法比较合适。

于是我们将上面的wait和notify方法调用包在同步块中。

 

			synchronized (flag) {
				flag = "false";
				flag.notify();
			}
 
			synchronized (flag) {
				while (flag != "false") {
					System.out.println(getName() + " begin waiting!");
					long startTime = System.currentTimeMillis();
					try {
						flag.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					long endTime = System.currentTimeMillis();
					System.out.println(getName() + " wai time: " + (endTime - startTime));
				}
				System.out.println(getName() + " end waiting!");
			}

 但是,运行这个程序,我们发现事与愿违。那个非法监视器异常 又出现了。。。

 

我们注意到,针对flag的同步块 中,我们实际上已经更改了flag对对象的引用: flag="false";

 

显然,这样一来,同步块也无能为力了,因为我们根本不是针对唯一的一个对象在进行同步

 

我们不妨将flag封装到JavaBean或者数组中去,这样用JavaBean对象或者数组对象进行同步 ,就可以达到既能修改里面参数又不耽误同步的目的。

private String[] flag = {"true"}; 
 
			synchronized (flag) {
				flag[0] = "false";
				flag.notify();
			}
 
			synchronized (flag) {
				while (flag[0] != "false") {
					System.out.println(getName() + " begin waiting!");
					long startTime = System.currentTimeMillis();
					try {
						flag.wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					long endTime = System.currentTimeMillis();
					System.out.println(getName() + " wait time: " + (endTime - startTime));
				}
				System.out.println(getName() + " end waiting!");
			}

 运行这个程序,看不到异常了 。但是仔细观察结果,貌似只有一个线程被唤醒 。利用jconsole等工具查看线程状态,发现的确还是有两个线程被阻塞的。这是为啥呢?

程序中使用了flag.notify()方法 。只能是随机的唤醒一个线程 。我们可以改用flag.notifyAll()方法 。这样,所有被阻塞的线程都会被唤醒了。

最终代码请读者自己修改,这里不再赘述。

好了,亲爱的读者们,让我们回到开篇提到的汉堡店大赛问题当中去,来看一看厨师、服务生和顾客是怎么协作进行这个比赛的。

首先我们构造故事中的三个次要对象:汉堡包、存放汉堡包的容器、服务生

 

package sky.cn.test4;
/**
 *	服务生,配角,不需要属性 
 */
public class Waiter {

}
 
package sky.cn.test4;
/**
 * 汉堡包 
 */
public class Hamberg {
	
	private int id; 				//汉堡编号
	private String cookerId;		//厨师编号
	
	public Hamberg(int id, String cookerId) {
		this.id = id;
		this.cookerId = cookerId;
		System.out.println(this.toString());
	}
	
	public String toString() {
		return "#hamberg " + id + "c" + cookerId + " makes by cooker " + cookerId;
	}
}
 
package sky.cn.test4;

import java.util.ArrayList;
import java.util.List;

/**
 * 汉堡包容器
 */
public class HambergFifo {
	List<Hamberg> hambergs = new ArrayList<Hamberg>();
	int maxSize = 10;
	
	//放入汉堡
	public <T extends Hamberg> void push(T t) {
		hambergs.add(t);
	}
	
	//取出汉堡
	public Hamberg pop() {
		Hamberg h = hambergs.get(0);
		hambergs.remove(0);
		return h;
	}
	
	//判断容器是否为空
	public synchronized boolean isEmpty() {
		return hambergs.isEmpty();
	}
	
	//判断容器内汉堡的个数
	public synchronized int size() {
		return hambergs.size();
	}
	
	//返回窗口的最大容量
	public synchronized int getMaxSize() {
		return this.maxSize;
	}
	
	//判断容器是否已满,未满为真
	public synchronized boolean isNotFull() {
		return hambergs.size() < this.maxSize;
	}
}
 

接下来我们构造厨师对象:

package sky.cn.test4;

/**
 * 厨师 
 */
public class Cooker implements Runnable {
	HambergFifo pool;		//厨师要面对容器
	Waiter waiter;			//还要面对服务生
	
	public Cooker(Waiter waiter, HambergFifo pool) {
		this.pool = pool;
		this.waiter = waiter;
	}
	
	//制造汉堡
	public void makeHamberg() {
		//制造的个数
		int madeCount = 0;
		//因为容器满,被迫等待的次数
		int fullFiredCount = 0;
		
		String threadName = Thread.currentThread().getName();
		
		try {
			while (true) {
				Thread.sleep(1000);
				synchronized (pool) {
					if (pool.isNotFull()) {
						synchronized (waiter) {
							//容器未满, 制作汉堡, 并放入容器
							pool.push(new Hamberg(++madeCount, threadName));
							//说出容器内汉堡数量
							System.out.println(threadName + ": There are "
									+ pool.size() + " hambergs in all");
							//让服务生通知顾客,有汉堡可以吃了
							waiter.notifyAll();
							System.out.println("### Cooker: waiter.notifyAll(): "
									+ " Hi!Customers, we got some new hambergs.");
							
						}
					} else {
						if (fullFiredCount++ < 10) {
							//发现容器满了,停止做汉堡的尝试
							System.out.println(threadName + " : Hamberg Pool is Full," +
									" stop making hamberg");
							
							System.out.println("### Cooker: pool.wait()");
							//汉堡容器的状况使厨师等待
							pool.wait();
						} else {
							return ;
						}
					}
				}
				
				//做完汉堡要进行收尾工作,为下一次的制作做准备
				Thread.sleep(1000);
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
			madeCount--;
		}
	}

	public void run() {
		makeHamberg();
	}
}
 

接下来,我们构造顾客对象:

package sky.cn.test4;

import java.util.Random;

/**
 *	顾客 
 */

public class Customer implements Runnable {
	
	Waiter waiter;					//顾客要面对服务生
	HambergFifo pool;				//也要面对汉堡包容器
	int ateCount = 0;				//想要记下自己吃了多少汉堡
	long sleepTime;					//吃每个汉堡的时间不尽相同
	Random r = new Random();		//用于产生随机数
	
	public Customer(Waiter waiter, HambergFifo pool) {
		this.waiter = waiter;
		this.pool = pool;
	}
	
	public void run() {
		while (true) {
			try {
				//取汉堡
				getHamberg();
				//吃汉堡
				eatHamberg();
			} catch (Exception e) {
				synchronized (waiter) {
					System.out.println(e.getMessage());
					//若取不到汉堡,要和服务生打交道
					try {
						System.out.println("###Customer: waiter.wait():" + 
								" Sorry, sir, there is no hambergs left, please wait");
						System.out.println(Thread.currentThread().getName() + 
								": OK, waiting for a new hamberg");
						//服务生安抚客户,让他等待
						waiter.wait();
						continue;
					} catch (InterruptedException ie) {
						ie.printStackTrace();
					}
				}
			}
		}
	}
	
	private void eatHamberg() {
		try {
			//吃每个汉堡的时间不等
			sleepTime = Math.abs(r.nextInt(3000)) * 5;
			System.out.println(Thread.currentThread().getName()
					+ " : I`m eating the hamberg for " + sleepTime 
					+ " milliseconds");
			
			Thread.sleep(sleepTime);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	private void getHamberg() {
		Hamberg hamberg = null;
		
		synchronized (pool) {
			try {
				//从容器内取汉堡
				hamberg = pool.pop();
				ateCount++;
				System.out.println(Thread.currentThread().getName() 
						+ ": I got " + ateCount + "th hamberg " + hamberg);
				System.out.println(Thread.currentThread().getName()
						+ ": There are still " + pool.size() + " hambergs left");
			} catch (Exception e) {
				pool.notifyAll();
				System.out.println("### Customer: pool.notifyAll()");
				throw new RuntimeException(Thread.currentThread().getName() + 
						": Oh my god!!! No hambergs left, waiter!" +
						" [Ring the bell besides the hamberg pool]");
			}
		}
	}

}

 最后,我们构造汉堡店,让这个故事发生:

package sky.cn.test4;

public class HambergShop {
	
	Waiter waiter = new Waiter();
	HambergFifo hambergPool = new HambergFifo();
	Customer customer = new Customer(waiter, hambergPool);
	Cooker cooker = new Cooker(waiter, hambergPool);
	
	public static void main(String[] args) {
		HambergShop hambergShop = new HambergShop();
		Thread t1 = new Thread(hambergShop.customer, "1");
		Thread t2 = new Thread(hambergShop.customer, "2");
		Thread t3 = new Thread(hambergShop.customer, "3");
		Thread t4 = new Thread(hambergShop.cooker, "1");
		Thread t5 = new Thread(hambergShop.cooker, "2");
		Thread t6 = new Thread(hambergShop.cooker, "3");
		
		t4.start();
		t5.start();
		t6.start();
		try {
			Thread.sleep(10000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		t1.start();
		t2.start();
		t3.start();
	}
}

 运行这个程序吧,然后你会看到我们汉堡店的比赛进行的很好,只是不知道那些顾客是不是会被撑到。。。

读到这里,有的读者可能会想到前面介绍的重入锁ReentrantLock。
有的读者会问:如果我用ReentrantLock来代替上面这些例程当中的 synchronized块,是不是也可以呢?感兴趣的读者不妨一试。

 

但是在这里,我想提前给出结论,就是,
如果用ReentrantLock的lock()和unlock()方法代替上面的synchronized块 ,那么上面这些程序还是要抛出 java.lang.IllegalMonitorStateException异常 的,不仅如此,你甚至还会看到线程死锁 。原因就是当某个线程调用第三 方对象的wait或者notify方法的时候,并没有进入第三方对象的监视器,于是抛出了异常信息 。但此时,程序流程如果没有用finally来处理 unlock方法,那么你的线程已经被lock方法上锁,并且无法解锁。程序在java.util.concurrent框架的语义级别死锁了 ,你用 JConsole这种工具来检测JVM死锁,还检测不出来。

 

正确的做法就是,只使用ReentrantLock,而不使用wait或者notify方法 。因为ReentrantLock已经对这种互斥和协作进行了概括。所以,根据你程序的需要,请单独采用重入锁或者synchronized一种同步机制,最好不要混用

 

好了,我们现在明白:

1. 线程的等待或者唤醒,并不是让线程调用自己的wait或者notify方法,而是通过调用线程共享对象的wait或者notify方法来实现。
2. 线程要调用某个对象的wait或者notify方法,必须先取得该对象的监视器。
3. 线程的协作必须以线程的互斥为前提,这种协作实际上是一种互斥下的协作

下一讲当中,我们来看看如何实实在在的解决线程之间抢占共享资源的问题。敬请期待!

 

来源: http://www.blogjava.net/zhangwei217245/archive/2010/04/24/316526.html

分享到:
评论

相关推荐

    JAVA实现线程间同步与互斥生产者消费者问题

    在Java编程中,线程同步和互斥是多线程编程中的重要概念,它们用于解决多个线程同时访问共享资源时可能出现的问题。本项目通过一个生产者消费者问题的实例,展示了如何在Java中实现线程间的同步与互斥。 生产者消费...

    操作系统实验 多线程同步与互斥 java编写 有界面

    在“操作系统实验 多线程同步与互斥 java编写 有界面”的实验中,可能需要设计一个图形用户界面(GUI),通过按钮或事件触发线程的创建和同步操作,直观地展示线程间的交互和同步效果。例如,可以模拟银行账户转账,...

    Java多线程wait和notify

    总结来说,Java的 `wait()` 和 `notify()` 提供了一种在多线程环境中控制线程执行的机制。通过合理使用这些方法,我们可以实现线程间的协作,精确控制子线程的运行状态。然而,这种方式虽然灵活,但管理起来相对复杂...

    java多线程设计模式详解(PDF及源码)

    wait set——线程的休息室 wait方法——把线程放入wait set notify方法——从wait set拿出线程 notifyAll方法——从wait set拿出所有线程 wait、notify、notifyAll是Object类的方法 线程的状态移转 跟线程有关的其他...

    java多线程教程——一个课件彻底搞清多线程

    本教程将深入讲解Java线程的相关知识,包括进程与线程的基本概念、线程的创建和启动、多线程的互斥与同步、线程状态和线程控制以及死锁的概念。 首先,我们要理解进程与线程的区别。进程是操作系统资源分配的基本...

    java 多线程 同步详解

    然而,多线程环境下数据的安全性问题不容忽视,这就引出了Java中的同步机制。本文将深入探讨Java多线程同步的核心概念,特别是`synchronized`关键字的使用,以及锁定对象与锁定类的区别。 1. **线程安全问题** 在...

    java 多线程编程实战指南(核心 + 设计模式 完整版)

    - **线程间通信**:`wait()`, `notify()` 和 `notifyAll()` 方法用于线程间的协作,需要在同步块或方法中使用。 3. **线程池** - **Executor框架**:`ExecutorService`、`ThreadPoolExecutor`和`Executors`工厂类...

    java多线程详解(比较详细的阐述了多线程机制)

    另外,wait()、notify()和notifyAll()方法用于线程间的通信,但它们必须在同步块或方法中使用,以确保正确唤醒等待的线程。 Java还引入了Lock接口和相关的实现,如ReentrantLock,提供比synchronized更细粒度的控制...

    java多线程代码案例(创建线程,主线程,线程优先级,线程组,线程同步,线程间的通信)

    本文将深入探讨Java多线程中的关键知识点,包括创建线程、主线程、线程优先级、线程组、线程同步以及线程间的通信。 1. **创建线程** 在Java中,可以通过两种方式创建线程:继承`Thread`类或实现`Runnable`接口。...

    Java多线程编程核心技术_完整版_java_

    1. wait()、notify()和notifyAll():这三个方法用于线程间的通信,它们必须在同步环境中使用。 2. Condition接口:配合Lock使用,提供更灵活的线程间通信方式。 五、线程池 1. Executor框架:Java 5引入的...

    Java多线程实现异步调用实例

    `wait()`, `notify()`和`notifyAll()`方法用于线程间的协作;`Thread.join()`可以让主线程等待子线程完成后再继续执行。 “异步”标签则涉及到了程序设计中的非阻塞特性,有助于提升系统的并发能力和响应性。Java 8...

    Java多线程编程

    4. **Object方法**:`Object`类中的几个方法在多线程环境下特别重要,如`wait()`, `notify()`, 和 `notifyAll()`。这些方法用于对象监视器机制,它们必须在同步块或方法中使用,否则会导致`...

    Java多线程的总结

    Java提供了多种线程间通信的方法,如wait()、notify()和notifyAll(),它们必须在同步块或同步方法中使用,用于控制线程的执行顺序。此外,还可以使用BlockingQueue阻塞队列实现生产者消费者模式,实现线程间的协作。...

    头歌java多线程基础-Java多线程基础详解与实战指南

    最后,探讨了线程间通信的方法,包括wait()、notify()、notifyAll()以及 BlockingQueue 的使用。通过丰富的代码示例,帮助读者理解和掌握Java多线程编程。 适合人群:具备基本Java编程知识,希望深入了解多线程编程...

    java多线程笔记全手打

    2. volatile关键字:确保多线程环境下变量的可见性和有序性,但不具备互斥性。 3. Lock接口与ReentrantLock类:提供比synchronized更细粒度的锁控制,支持公平锁和非公平锁,以及可重入和可中断特性。 四、线程通信...

    Java多线程管理示例

    下面我们将深入探讨Java多线程的核心概念、同步机制、死锁问题以及wait/notify机制,以"生产者与消费者"的例子来具体阐述。 首先,了解Java中的线程。线程是操作系统分配CPU时间的基本单位,每个线程都有自己的程序...

    java多线程实现生产者和消费者

    3. **同步控制**:Java提供了多种同步工具,如`synchronized`关键字、`wait()`, `notify()`和`notifyAll()`方法,以及`java.util.concurrent`包下的`BlockingQueue`接口。`synchronized`用于互斥访问,`wait()`, `...

    Java多线程下载网络图片

    在Java编程中,多线程是一项关键技能,尤其在处理并发任务时,如我们的示例——"Java多线程下载网络图片"。这个场景展示了如何利用多线程技术提高程序性能,减少用户等待时间,同时优化系统资源的使用。下面我们将...

    java 多线程编程指南

    Java提供了wait()、notify()和notifyAll()方法,这些方法与synchronized配合使用,可以让线程在特定条件下等待或唤醒。另外,Java并发包(java.util.concurrent)提供了更高级的并发工具,如Semaphore(信号量)、...

Global site tag (gtag.js) - Google Analytics