Java并发编程系列之三十一 过早的通知

                     

等待通知机制

在前面介绍了等待通知机制,并使用该机制实现了消费者-生产者模式。我们知道,一个因为调用wait的线程会进入等待队列,当有其他的线程通知的时候才会从等待队列中返回,线程状态会变为RUNNABLE。但是,反过来说,如果一个线程从wait方法中返回,是不是就一定意味着线程等待的条件满足了呢?答案是否定的。考虑这样的场景:比如两个人的手机铃声是一样的(音量和类型),那么当两个手机同时响的时候,就不能正确判断哪个响的手机是自己的。而且线程从wait方法返回完成可能是意外导致的。

从线程的角度分析,每次调用wait方法的前提必然是首先获得了锁,然后会因为某个等待条件去调用wait方法,调用wait方法的时候会释放锁的持有。那么,当线程重新进入的调用wait方法的代码时,等待的条件就不一定满足了,那么继续往下执行就会出现错误的结果。比如,在执行notify通知的线程调用notify方法时,等待的条件是成立的,但是当线程重新获得锁的时候等待条件却是假的。出现这种情况的根源在于从调用notify进行唤醒并释放锁,到线程重新获取锁的这个时间内,如果有其他线程修改了等待条件,这种情况就出现了。

以上的现象称为“过早的通知”,为了更好理解这种现象,看看下面的代码就知道了:

package com.rhwayfun.patchwork.concurrency.r0414;import java.text.DateFormat;import java.text.SimpleDateFormat;import java.util.ArrayList;import java.util.Date;import java.util.List;import java.util.concurrent.TimeUnit;import java.util.concurrent.atomic.AtomicLong;/** * Created by rhwayfun on 16-4-14. */public class EarlySignalDemo {    //元素列表    private List<String> list;    //日期格式器    private static final DateFormat format = new SimpleDateFormat("HH:mm:ss");    //计数器    private AtomicLong number = new AtomicLong();    public EarlySignalDemo() {        list = new ArrayList<>();    }    //对list执行删除的元素    public void remove() throws InterruptedException {        synchronized (list){            if (list.isEmpty()){                //只要list为空,那么调用此方法的线程必须等待                list.wait();            }            //如果执行到这里,说明list已经不为空了            //这样执行元素的删除操作才不会出错            String item = list.remove(0);            System.out.println(Thread.currentThread().getName() + ": remove element " + item + "! "                + format.format(new Date()));        }    }    //对list执行添加操作    public void add(){        synchronized (list){            //添加元素不要进行判断            list.add(""+ number.incrementAndGet());            System.out.println(Thread.currentThread().getName() + ": add item " + number.get()                + " " +format.format(new Date()));            list.notifyAll();        }    }    static class AddThread implements Runnable{        private EarlySignalDemo es;        public AddThread(EarlySignalDemo es) {            this.es = es;        }        @Override        public void run() {            try {                TimeUnit.MILLISECONDS.sleep(600);                es.add();            } catch (InterruptedException e) {                e.printStackTrace();            }        }    }    static class RemoveThread implements Runnable{        private EarlySignalDemo es;        public RemoveThread(EarlySignalDemo es) {            this.es = es;        }        @Override        public void run() {            try {                TimeUnit.MILLISECONDS.sleep(100);                es.remove();            } catch (InterruptedException e) {                e.printStackTrace();            }        }    }    public static void main(String[] args){        EarlySignalDemo es = new EarlySignalDemo();        for (int i = 0; i < 3; i++){            new Thread(new RemoveThread(es),"RemoveThread" + i).start();        }        new Thread(new AddThread(es),"AddThread").start();    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97

运行的结果如下:

Java并发编程系列之三十一 过早的通知

程序出现了数组下标越界的错误,简单计算一下,3个RemoveThread的等待时间之和是300毫秒,而AddThread需要600毫秒之后才会执行,所以在600毫秒之前,所有的RemoveThread都因为等待条件list为空陷入等待,进入等待队列中。当执行到600毫秒的时候,唤醒全部的RemoveThread,从wait返回的RemoveThread不会重新判断list的等待条件,这样造成的后果就是三个RemoveThread同时删除list中的一个元素,自然就会出现下标越界错误了。也正是3个RemoveThread在被唤醒到重新获得锁的期间等待条件被修改了,导致出现了错误的结果。

更正的办法就是把remove方法中对list是否为空的判断改为while循环就可以了。

小结
当使用条件等待时,往往需要对等待条件进行循环测试,避免过早的通知。

           

再分享一下我老师大神的人工智能教程吧。零基础!通俗易懂!风趣幽默!还带黄段子!希望你也加入到我们人工智能的队伍中来!https://blog.****.net/jiangjunshow

                     

等待通知机制

在前面介绍了等待通知机制,并使用该机制实现了消费者-生产者模式。我们知道,一个因为调用wait的线程会进入等待队列,当有其他的线程通知的时候才会从等待队列中返回,线程状态会变为RUNNABLE。但是,反过来说,如果一个线程从wait方法中返回,是不是就一定意味着线程等待的条件满足了呢?答案是否定的。考虑这样的场景:比如两个人的手机铃声是一样的(音量和类型),那么当两个手机同时响的时候,就不能正确判断哪个响的手机是自己的。而且线程从wait方法返回完成可能是意外导致的。

从线程的角度分析,每次调用wait方法的前提必然是首先获得了锁,然后会因为某个等待条件去调用wait方法,调用wait方法的时候会释放锁的持有。那么,当线程重新进入的调用wait方法的代码时,等待的条件就不一定满足了,那么继续往下执行就会出现错误的结果。比如,在执行notify通知的线程调用notify方法时,等待的条件是成立的,但是当线程重新获得锁的时候等待条件却是假的。出现这种情况的根源在于从调用notify进行唤醒并释放锁,到线程重新获取锁的这个时间内,如果有其他线程修改了等待条件,这种情况就出现了。

以上的现象称为“过早的通知”,为了更好理解这种现象,看看下面的代码就知道了:

package com.rhwayfun.patchwork.concurrency.r0414;import java.text.DateFormat;import java.text.SimpleDateFormat;import java.util.ArrayList;import java.util.Date;import java.util.List;import java.util.concurrent.TimeUnit;import java.util.concurrent.atomic.AtomicLong;/** * Created by rhwayfun on 16-4-14. */public class EarlySignalDemo {    //元素列表    private List<String> list;    //日期格式器    private static final DateFormat format = new SimpleDateFormat("HH:mm:ss");    //计数器    private AtomicLong number = new AtomicLong();    public EarlySignalDemo() {        list = new ArrayList<>();    }    //对list执行删除的元素    public void remove() throws InterruptedException {        synchronized (list){            if (list.isEmpty()){                //只要list为空,那么调用此方法的线程必须等待                list.wait();            }            //如果执行到这里,说明list已经不为空了            //这样执行元素的删除操作才不会出错            String item = list.remove(0);            System.out.println(Thread.currentThread().getName() + ": remove element " + item + "! "                + format.format(new Date()));        }    }    //对list执行添加操作    public void add(){        synchronized (list){            //添加元素不要进行判断            list.add(""+ number.incrementAndGet());            System.out.println(Thread.currentThread().getName() + ": add item " + number.get()                + " " +format.format(new Date()));            list.notifyAll();        }    }    static class AddThread implements Runnable{        private EarlySignalDemo es;        public AddThread(EarlySignalDemo es) {            this.es = es;        }        @Override        public void run() {            try {                TimeUnit.MILLISECONDS.sleep(600);                es.add();            } catch (InterruptedException e) {                e.printStackTrace();            }        }    }    static class RemoveThread implements Runnable{        private EarlySignalDemo es;        public RemoveThread(EarlySignalDemo es) {            this.es = es;        }        @Override        public void run() {            try {                TimeUnit.MILLISECONDS.sleep(100);                es.remove();            } catch (InterruptedException e) {                e.printStackTrace();            }        }    }    public static void main(String[] args){        EarlySignalDemo es = new EarlySignalDemo();        for (int i = 0; i < 3; i++){            new Thread(new RemoveThread(es),"RemoveThread" + i).start();        }        new Thread(new AddThread(es),"AddThread").start();    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97

运行的结果如下:

Java并发编程系列之三十一 过早的通知

程序出现了数组下标越界的错误,简单计算一下,3个RemoveThread的等待时间之和是300毫秒,而AddThread需要600毫秒之后才会执行,所以在600毫秒之前,所有的RemoveThread都因为等待条件list为空陷入等待,进入等待队列中。当执行到600毫秒的时候,唤醒全部的RemoveThread,从wait返回的RemoveThread不会重新判断list的等待条件,这样造成的后果就是三个RemoveThread同时删除list中的一个元素,自然就会出现下标越界错误了。也正是3个RemoveThread在被唤醒到重新获得锁的期间等待条件被修改了,导致出现了错误的结果。

更正的办法就是把remove方法中对list是否为空的判断改为while循环就可以了。

小结
当使用条件等待时,往往需要对等待条件进行循环测试,避免过早的通知。