澳门新葡萄京官网首页 3

Java 并发协作 wait、notify、notifyAll 方法

wait, notify 和
notifyAll,这些在多线程中被经常用到的保留关键字,在实际开发的时候很多时候却并没有被大家重视。本文对这些关键字的使用进行了描述。

问:简单谈谈 Java 并发协作的 wait、notify、notifyAll 等方法的特点和场景?

答:首先并发协作的 wait、notify、notifyAll 等方法是定义在 java 的
Object 类中,而非 Thread。wait 有一个重载方法,参数 0
表示无限等待,更加重要的是在等待期间均可被中断并抛出
InterruptedException(很重要)。

每个对象都有一把锁和等待队列,线程在进入 synchronized
时,如果尝试获取锁但失败,就会把当前线程加入锁的等待队列,其实每个对象除过有用于锁的等待队列外还有一个条件队列,条件队列就是用来进行线程间的协作的。调用
wait 方法就会把当前线程放入这个条件队列并阻塞,然后等待其他线程通过
notify 或者 notifyAll 触发这个条件(自己无法触发)来执行,惟一的区别就是
notify 会从条件队列选择一个线程触发条件并且从队列移除,而 notifyAll
会触发条件队列里所有等待的线程并从队列移除。

wait 和 notify、notifyAll 只能在 synchronized
函数或者对象中调用,被上锁的对象一般是多线程共享的对象,如果调用 wait
和 notify、notifyAll 方法时当前线程没有持有对象锁则会抛出
IllegalMonitorStateException 异常。

切记代码执行到 synchronized 锁起来的 wait
方法时当前线程会释放对象锁,
因为 wait
的具体实现过程是先把当前线程放入条件等待队列、释放对象锁、阻塞等待(线程状态变为
WAITING 或 TIMED_WATING),等待时间到了或者被其他线程 notify、notifyAll
以后从条件队列中移除。然后要重新竞争对象锁,竞争到就变为 RUNNABLE
状态,否则该线程被加入对象锁队列变为 BLOCKED 状态。

切记调用 notify、notifyAll
会把条件队列中等待的线程移除但是不会释放对象锁,只有在包含
notify、notifyAll 的 synchronized
方法或者代码块执行完毕才能轮到等待的线程执行。

除了我们要保证 wait 和 notify、notifyAll 应该在 synchronized
块中和那个被多线程共享的对象上调用以外,还要尽可能保证永远在条件循环而不是
if 语句中使用 wait,因为线程从 wait
调用中返回后不代表其等待的条件就一定成立,
所以我们在使用 wait
时应该尽量使用如下模板:

        synchronized (sharedObject) {
            while (condition) {
                sharedObject.wait();
                // (Releases lock, and reacquires on wakeup)
            }
            // do action based upon condition e.g. take or put into queue
        }
    }

在条件循环里使用 wait
的目的是在线程被唤醒的前后,都持续检查条件是否被满足,如果条件并未改变而
wait 被调用之前 notify
的唤醒通知就来了,那么这个线程并不能保证被唤醒且有可能会导致死锁问题(建立在全局项目超过两个线程以上)。

譬如假设有两个生产者 A、B,一个消费者 C,在生产消费者模式中如果对生产者
A、B 不使用条件循环而简单 if 判断中调用 wait 就会出事,当空间满了后 A、B
都被 wait,当 C 取走一个数据后如果调用了 notifyAll 则 A、B
都将被唤醒,假设 A 被唤醒后往空间放入一个数据且空间满了,而此时 B
也会放置一个数据,所以发生空间炸裂错误。

(提示:如上也解答了并发的另一个面试题,即 Java 多线程为什么使用 while
循环来调用 wait 方法?)其实 Thread 的 join
方法实现也是条件循环,核心代码是:

while (isAlive()) {
  lock.wait(0);
  }

澳门新葡萄京官网首页 ,并发协作其实在 java.util.concurrent
包下已经提供了很多不错且高效的封装实现类了,不过我们依然可以自己使用
wait 和 notify、notifyAll 来解决生产消费者场景、并发等待等场景问题。

1、synchronized关键字
  
JAVA的进程同步是通过synchronized()来实现的。需要说明的是,JAVA的synchronized()方法类似于操作系统概念中的互斥内存块,在JAVA中的Object类型中,都是带有一个内存锁的,在有线程获取该内存锁后,其它线程无法访问该内存,从而实现JAVA中简单的同步、互斥操作。
  理解了这个,就能理解为什么synchronized(this)与synchronized(static
XXX)的区别了。synchronized就是针对内存区块申请内存锁,this关键字代表类的一个对象,所以其内存锁是针对相同对象的互斥操作,而static成员属于类专有,其内存空间为该类所有成员共有,这就导致synchronized()对static成员加锁,相当于对类加锁,也就是在该类的所有成员间实现互斥,在同一时间只有一个线程可访问该类的实例。如果只是简单的想要实现在JAVA中的线程互斥,明白这些基本就已经够了。但如果需要在线程间相互唤醒的话就需要借助Object.wait(),
Object.nofity()了。

在 Java 中可以用 wait、notify 和 notifyAll
来实现线程间的通信。。举个例子,如果你的Java程序中有两个线程——即生产者和消费者,那么生产者可以通知消费者,让消费者开始消耗数据,因为队列缓冲区中有内容待消费(不为空)。相应的,消费者可以通知生产者可以开始生成更多的数据,因为当它消耗掉某些数据后缓冲区不再为满。

2、wait(),sleep()和notify()方法
  Obj.wait()与Obj.notify()必须要与synchronized(Obj)一起使用。也就是wait与notify是针对已经获取了Obj锁进行操作。
  从语法角度来说就是Obj.wait(),Obj.notify必须在synchronized(Obj){…}语句块内;从功能上来说wait就是说线程在获取对象锁后,主动释放对象锁,同时本线程休眠。直到有其它线程调用对象的notify()唤醒该线程,才能继续获取对象锁,并继续执行。相应的notify()就是对对象锁的唤醒操作。但有一点需要注意的是notify()调用后,并不是马上就释放对象锁的,而是在相应的synchronized(){}语句块执行结束,自动释放锁后,JVM会在wait()对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执行。这样就提供了在线程间同步、唤醒的操作。
  Thread.sleep()与Object.wait()二者都可以暂停当前线程,释放CPU控制权,主要的区别在于Object.wait()在释放CPU同时,释放了对象锁的控制。

我们可以利用wait()来让一个线程在某些条件下暂停运行。例如,在生产者消费者模型中,生产者线程在缓冲区为满的时候,消费者在缓冲区为空的时候,都应该暂停运行。如果某些线程在等待某些条件触发,那当那些条件为真时,你可以用
notify 和 notifyAll
来通知那些等待中的线程重新开始运行。不同之处在于,notify
仅仅通知一个线程,并且我们不知道哪个线程会收到通知,然而 notifyAll
会通知所有等待中的线程。换言之,如果只有一个线程在等待一个信号灯,notify和notifyAll都会通知到这个线程。但如果多个线程在等待这个信号灯,那么notify只会通知到其中一个,而其它线程并不会收到任何通知,而notifyAll会唤醒所有等待中的线程。

3、Java中的wait()为什么总是放在while中,而不是if语句中?
这是由于其它线程(与你的wait()完全无关的其它线程)会调用notifyAll(),而这个线程并不是你的wait()所共同协作的那个线程。因此,每一次被唤醒,你都要检查一次你wait()等待的条件。因而要用while。

在这篇文章中你将会学到如何使用 wait、notify 和 notifyAll
来实现线程间的通信,从而解决生产者消费者问题。如果你想要更深入地学习Java中的多线程同步问题,我强烈推荐阅读Brian
Goetz所著的《Java Concurrency in
Practice | Java
并发实践》,不读这本书你的
Java 多线程征程就不完整哦!这是我最向Java开发者推荐的书之一。

if(条件不满足){
    this.wait();
}
....其他语句

如何使用Wait

尽管关于wait和notify的概念很基础,它们也都是Object类的函数,但用它们来写代码却并不简单。如果你在面试中让应聘者来手写代码,用wait和notify解决生产者消费者问题,我几乎可以肯定他们中的大多数都会无所适从或者犯下一些错误,例如在错误的地方使用
synchronized
关键词,没有对正确的对象使用wait,或者没有遵循规范的代码方法。说实话,这个问题对于不常使用它们的程序员来说确实令人感觉比较头疼。

第一个问题就是,我们怎么在代码里使用wait()呢?因为wait()并不是Thread类下的函数,我们并不能使用Thread.call()。事实上很多Java程序员都喜欢这么写,因为它们习惯了使用Thread.sleep(),所以他们会试图使用wait()
来达成相同的目的,但很快他们就会发现这并不能顺利解决问题。正确的方法是对在多线程间共享的那个Object来使用wait。在生产者消费者问题中,这个共享的Object就是那个缓冲区队列。

第二个问题是,既然我们应该在synchronized的函数或是对象里调用wait,那哪个对象应该被synchronized呢?答案是,那个你希望上锁的对象就应该被synchronized,即那个在多个线程间被共享的对象。在生产者消费者问题中,应该被synchronized的就是那个缓冲区队列。(我觉得这里是英文原文有问题……本来那个句末就不应该是问号不然不太通……)

澳门新葡萄京官网首页 1

当其他线程notify()或者notifyAll()的时候,你继续执行其他的语句
这个时候,也许你的条件依然不满足,所以需要:

永远在循环(loop)里调用 wait 和 notify,不是在 If 语句

现在你知道wait应该永远在被synchronized的背景下和那个被多线程共享的对象上调用,下一个一定要记住的问题就是,你应该永远在while循环,而不是if语句中调用wait。因为线程是在某些条件下等待的——在我们的例子里,即“如果缓冲区队列是满的话,那么生产者线程应该等待”,你可能直觉就会写一个if语句。但if语句存在一些微妙的小问题,导致即使条件没被满足,你的线程你也有可能被错误地唤醒。所以如果你不在线程被唤醒后再次使用while循环检查唤醒条件是否被满足,你的程序就有可能会出错——例如在缓冲区为满的时候生产者继续生成数据,或者缓冲区为空的时候消费者开始小号数据。所以记住,永远在while循环而不是if语句中使用wait!我会推荐阅读《Effective
Java》,这是关于如何正确使用wait和notify的最好的参考资料。

基于以上认知,下面这个是使用wait和notify函数的规范代码模板:

// The standard idiom for calling the wait method in Java 
synchronized (sharedObject) { 
    while (condition) { 
    sharedObject.wait(); 
        // (Releases lock, and reacquires on wakeup) 
    } 
    // do action based upon condition e.g. take or put into queue 
}

就像我之前说的一样,在while循环里使用wait的目的,是在线程被唤醒的前后都持续检查条件是否被满足。如果条件并未改变,wait被调用之前notify的唤醒通知就来了,那么这个线程并不能保证被唤醒,有可能会导致死锁问题。

while(条件不满足){
    this.wait();
}
.....其他语句

Java wait(), notify(), notifyAll() 范例

4、注意
(1)可以使用wait和notify函数来实现线程间通信。可以用它们来实现多线程(>3)之间的通信。
(2)永远在synchronized的函数或对象里使用wait、notify和notifyAll,不然Java虚拟机会生成
IllegalMonitorStateException。
(3)永远在while循环里而不是if语句下使用wait。这样,循环会在线程睡眠前后都检查wait的条件,并在条件实际上并未改变的情况下处理唤醒通知。
(4)永远在多线程间共享的对象(在生产者消费者模型里即缓冲区队列)上使用wait。
(5)倾向用 notifyAll()(唤醒所有等待中的线程),而不是
notify()(唤醒其中一个,而其它线程并不会收到任何通知被唤醒)。

下面我们提供一个使用wait和notify的范例程序。在这个程序里,我们使用了上文所述的一些代码规范。我们有两个线程,分别名为PRODUCER(生产者)和CONSUMER(消费者),他们分别继承了了Producer和Consumer类,而Producer和Consumer都继承了Thread类。Producer和Consumer想要实现的代码逻辑都在run()函数内。Main线程开始了生产者和消费者线程,并声明了一个LinkedList作为缓冲区队列(在Java中,LinkedList实现了队列的接口)。生产者在无限循环中持续往LinkedList里插入随机整数直到LinkedList满。我们在while(queue.size

maxSize)循环语句中检查这个条件。请注意到我们在做这个检查条件之前已经在队列对象上使用了synchronized关键词,因而其它线程不能在我们检查条件时改变这个队列。如果队列满了,那么PRODUCER线程会在CONSUMER线程消耗掉队列里的任意一个整数,并用notify来通知PRODUCER线程之前持续等待。在我们的例子中,wait和notify都是使用在同一个共享对象上的。

import java.util.LinkedList; 
import java.util.Queue; 
import java.util.Random; 
/** 
* Simple Java program to demonstrate How to use wait, notify and notifyAll() 
* method in Java by solving producer consumer problem.
* 
* @author Javin Paul 
*/ 
public class ProducerConsumerInJava { 
    public static void main(String args[]) { 
        System.out.println("How to use wait and notify method in Java"); 
        System.out.println("Solving Producer Consumper Problem"); 
        Queue<Integer> buffer = new LinkedList<>(); 
        int maxSize = 10; 
        Thread producer = new Producer(buffer, maxSize, "PRODUCER"); 
        Thread consumer = new Consumer(buffer, maxSize, "CONSUMER"); 
        producer.start(); consumer.start(); } 
    } 
    /** 
    * Producer Thread will keep producing values for Consumer 
    * to consumer. It will use wait() method when Queue is full 
    * and use notify() method to send notification to Consumer 
    * Thread. 
    * 
    * @author WINDOWS 8 
    * 
    */ 
    class Producer extends Thread 
    { private Queue<Integer> queue; 
        private int maxSize; 
        public Producer(Queue<Integer> queue, int maxSize, String name){ 
            super(name); this.queue = queue; this.maxSize = maxSize; 
        } 
        @Override public void run() 
        { 
            while (true) 
                { 
                    synchronized (queue) { 
                        while (queue.size() == maxSize) { 
                            try { 
                                System.out .println("Queue is full, " + "Producer thread waiting for " + "consumer to take something from queue"); 
                                queue.wait(); 
                            } catch (Exception ex) { 
                                ex.printStackTrace(); } 
                            } 
                            Random random = new Random(); 
                            int i = random.nextInt(); 
                            System.out.println("Producing value : " + i); queue.add(i); queue.notifyAll(); 
                        } 
                    } 
                } 
            } 
    /** 
    * Consumer Thread will consumer values form shared queue. 
    * It will also use wait() method to wait if queue is 
    * empty. It will also use notify method to send 
    * notification to producer thread after consuming values 
    * from queue. 
    * 
    * @author WINDOWS 8 
    * 
    */ 
    class Consumer extends Thread { 
        private Queue<Integer> queue; 
        private int maxSize; 
        public Consumer(Queue<Integer> queue, int maxSize, String name){ 
            super(name); 
            this.queue = queue; 
            this.maxSize = maxSize; 
        } 
        @Override public void run() { 
            while (true) { 
                synchronized (queue) { 
                    while (queue.isEmpty()) { 
                        System.out.println("Queue is empty," + "Consumer thread is waiting" + " for producer thread to put something in queue"); 
                        try { 
                            queue.wait(); 
                        } catch (Exception ex) { 
                            ex.printStackTrace(); 
                        } 
                    } 
                    System.out.println("Consuming value : " + queue.remove()); queue.notifyAll(); 
                } 
            } 
        } 
    }

澳门新葡萄京官网首页 2

为了更好地理解这个程序,我建议你在debug模式里跑这个程序。一旦你在debug模式下启动程序,它会停止在PRODUCER或者CONSUMER线程上,取决于哪个线程占据了CPU。因为两个线程都有wait()的条件,它们一定会停止,然后你就可以跑这个程序然后看发生什么了(很有可能它就会输出我们以上展示的内容)。你也可以使用Eclipse里的Step
into和Step over按钮来更好地理解多线程间发生的事情。

5、使用实例

本文重点:

1.
你可以使用wait和notify函数来实现线程间通信。你可以用它们来实现多线程(>3)之间的通信。

2.
永远在synchronized的函数或对象里使用wait、notify和notifyAll,不然Java虚拟机会生成
IllegalMonitorStateException。

3.
永远在while循环里而不是if语句下使用wait。这样,循环会在线程睡眠前后都检查wait的条件,并在条件实际上并未改变的情况下处理唤醒通知。

4.
永远在多线程间共享的对象(在生产者消费者模型里即缓冲区队列)上使用wait。

5. 基于前文提及的理由,更倾向用 notifyAll(),而不是 notify()。

澳门新葡萄京官网首页 3

这是关于Java里如何使用wait,
notify和notifyAll的所有重点啦。你应该只在你知道自己要做什么的情况下使用这些函数,不然Java里还有很多其它的用来解决同步问题的方案。例如,如果你想使用生产者消费者模型的话,你也可以使用BlockingQueue,它会帮你处理所有的线程安全问题和流程控制。如果你想要某一个线程等待另一个线程做出反馈再继续运行,你也可以使用CycliBarrier或者CountDownLatch。如果你只是想保护某一个资源的话,你也可以使用Semaphore。

//线程A
synchronized(obj) {
  while(!condition) {
    obj.wait();
  }
  obj.doSomething();
}

当线程A获得了obj锁后,发现条件condition不满足,无法继续下一处理,于是线程A就wait()
, 放弃对象锁。
之后在另一线程B中,如果B更改了某些条件,使得线程A的condition条件满足了,就可以唤醒线程A:

//线程B
synchronized(obj) {
  condition = true;
  obj.notify();
}

发表评论

电子邮件地址不会被公开。 必填项已用*标注