java线程间的通信
一、wait和notify方法详解
wait和notify方法并不是Thread特有的方法,而是Object中的方法,也就是说在JDK中的每个类都有这2个方法。
1.下面是wait方法的三个重载方法:
public final void wait() throws InterruptedException
public final void wait(long timeout) throws InterruptedException
public final void wait(long timeout,int nanos) throws InterruptedException
Object 的wait(long timeout)方法会导致当前线程进入阻塞,直到有其他线程调用了Object的notify或者notifyAll方法才能将其唤醒。
wait方法必须拥有该对象的monitor,也就是wait方法必须要在同步方法中使用。
当前线程执行了对象的wait方法之后,将会放弃对该monitor的所有权。并且 进入与该对象关联的wait set中,也就是说一旦线程执行了某个对象的wait方法之后,它就会释放对该对象的monitor的所有权,其他线程会继续争抢该monitord的所有权。
2.notify方法
唤醒单个正在执行该对象wait方法的线程
如果有某个线程由于执行该对象的wait方法而进入了阻塞则会被唤醒,如果没有则会被忽略。
被唤醒的线程需要重新获取对该对象所关联monitor的lock才能继续执行。
3.关于wait和notify的注意事项
wait方法是可中断方法,这也就意味着当前线程一旦调用了wait方法进入阻塞状态,其他线程是可以使用interrupt方法将其打断的;可中断方法被打断后会收到中断异常InterruptedException,同时interrupt标识也会被擦除。
线程执行了某个对象的wait方法之后,会加入与之对应的wait set中,每一个对象的monitor都会有一个与之关联的wait set.
当线程进入wait set后,notify方法可以将其唤醒,也就是从wait set中弹出,同时中断wait中的线程也会将其唤醒。
必须在同步方法中使用wait和notify方法,因为执行wait和notify的前提条件是必须有同步方法的monitor的所有权。
同步代码的monitor必须与执行wait notify方法的对象一致,简单地说就是用哪个对象的monitor进行同步,就只能用哪个对象进行wait和notify的操作。
4、下面是一个wait和notify方法在单线程中的具体应用
package com.kafka.testThread;
import java.util.LinkedList;
public class EventQueue {
private final int max;
static class Event{
}
private final LinkedList<Event>eventQueue = new LinkedList<>();
private final static int DEFAULT_MAX_EVENT = 10;
public EventQueue(){
this(DEFAULT_MAX_EVENT);
}
public EventQueue(int max){
this.max = max;
}
public void offer(Event event){
synchronized (eventQueue){ //对象锁,和执行wait和notify方法的对象一致
if(eventQueue.size() >= max){
try {
console("the queue is full.");
eventQueue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
console(" the new event is submitted");
eventQueue.addLast(event);
eventQueue.notify();//唤醒阻塞的线程,告诉take线程可以往外取数据了
}
}
public Event take() throws InterruptedException {
synchronized (eventQueue){
if(eventQueue.isEmpty()){
console(" the queue is empty.");
eventQueue.wait();
}
Event event = eventQueue.removeFirst();
this.eventQueue.notify();//唤醒线程可以添加数据了
console(" the event "+ event + "is handled.");
return event;
}
}
private void console(String message){
System.out.println(Thread.currentThread().getName()+":"+message);
}
}
package com.kafka.testThread;
import java.util.concurrent.TimeUnit;
public class EventClient {
public static void main(String[] args) {
final EventQueue eventQueue = new EventQueue();
new Thread(new Runnable() {
@Override
public void run() {
for(; ;){
eventQueue.offer(new EventQueue.Event());
}
}
},"Producer").start();
new Thread(new Runnable() {
@Override
public void run() {
for(; ;){
try {
eventQueue.take();
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"Consumer").start();
}
}
以上程序模拟了生产者和消费者模式下,向queue中添加和取数的过程,过程中生产者提交客户端几乎没有延迟,而消费端,模拟带有一定的延迟。
5、多线程间通信要注意的事项
在4中,我们模拟了单线程下wait和notify的使用,如果多个线程同时进行take或者offer,那么上面的程序就会出现问题。
在多线程操作时,就不能使用notify方法了,要用notifyAll方法。notifyAll方法是同时唤醒所有的阻塞线程,同样被唤醒的线程仍需要继续争抢monitor的锁。
多线程下会出现以下2个问题:
notify方法只能唤醒一个线程,在上面代码中,虽然使用了同步关键字,进行数据同步,依旧会出现数据不一致的问题。假设EvenQueue中的元素为空,两个线程在执行take方法时,分别调用wait方法进入到了阻塞之中,另外一个offer线程执行addList方法后,向queue中添加了一个元素后,唤醒了其中一个阻塞的线程take,该线程顺利消费了一个元素之后恰巧再次唤醒了一个take线程(注意此时再次唤醒又是一个take线程,因为是同一个对象执行的notify方法,不能区分是哪个线程的唤醒方法。)这时候就可能出现LinkedList为空了,仍执行了removeFirst方法。
另外一种情况是:两个线程在执行offer方法的时候分别调用了wait方法而进入了阻塞中,即队列中达到了最大值之后,调用了阻塞方法wait,另外一个线程执行take方法消费了event元素并且唤醒了一个offer线程,而 该线程执行addList方法之后,queue中的元素为10,又唤醒了一个offer进程执行了addList方法,那么这个时候queue中的元素就超过了设定的max.
此时,针对在多线程下出现的以上2个问题就需要对程序做出修改:
1.把对是否进入阻塞状态判断的条件if方法改为while
2.对象的唤醒方法由notify改为notifyAll
个人理解:if改为while后,程序会循环判断当前线程是否满足进入wait阻塞状态的条件,当其中一个wait线程被唤醒后,第一次执行完while内程序后会再次去判断是否满足while里的条件。这样就会避免了上面那两个问题。
notify改为notifyAll,执行notifyAll后唤醒所有被wait阻塞的进程。让所有的阻塞线程都退出wait状态,重新去争抢同步锁。如果用notify只去唤醒一个线程,因为不能区分到底是生产者的线程还是消费者的线程,如果在生产者阻塞线程情况下,消费者消费完一个数据后,唤醒了一个生产者,生产者在queue里添加了一个元素后调用notify,此时的唤醒如果调用的一直是生产者的阻塞线程,那么程序就会陷入死锁状态。而我们设计生产者消费者模式的程序,在生产者生产了一个数据到queue后,再次唤醒一个线程是为了让消费者唤醒去消费,所以要用notifyAll方法去唤醒所有的wait线程,此时处于阻塞的线程执行完本轮循环后会由阻塞池转入到锁池中去,争抢锁资源。
6. sleep和wait
- sleep :
sleep是Thread的静态方法,sleep方法不会释放lock,可以从任何上下文调用
- wait:
wait是Object的实例方法,wait会释放锁,而且会加入到队列中等待唤醒,必须在synchronized的块下来使用
还没有评论,来说两句吧...