生产-消费模型之虚假唤醒

1 何为虚假唤醒

当线程从等待状态中被唤醒时,只是发现未满足其正在等待的条件时,就会发生虚假唤醒。 之所以称其为虚假的,是因为该线程似乎无缘无故被唤醒。 虚假唤醒不会无缘无故发生,通常是因为在发起唤醒号和等待线程最终运行之间的临界时间内,线程不再满足竞态条件。

2 java中的例子


public class SpuriousWakeupRWLock {
    private static final CustomQueue CUSTOM_LIST = new CustomQueue();

    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 8; i++) {
            Thread consumer = new Thread(() -> {
                try {
                    CUSTOM_LIST.getOne();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ;
            });
            consumer.setName("consumer:[" + i + "]");
            consumer.start();
        }
        TimeUnit.SECONDS.sleep(8);
        for (int i = 0; i < 2 ; i++) {
            int finalI = i;
            Thread producer = new Thread(() -> {
                try {
                    CUSTOM_LIST.putOne(finalI);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ;
            });
            producer.setName("producer : [" + i + "]");
            producer.start();
        }




    }

    static class CustomQueue {

        private List<Integer> data = new ArrayList<>();

        public  synchronized  void getOne() throws InterruptedException {
            if (data.size() <= 0) {
                System.out.println(Thread.currentThread() + " : wait");
                TimeUnit.SECONDS.sleep(1);
                this.wait();
            }
            Integer number = data.get(0);
            data.remove(0);
            System.out.println(Thread.currentThread() + "-----> get one :" + number);
            this.notifyAll();
        }

        public synchronized void putOne(int number) throws InterruptedException {
            if (data.size() == Integer.MAX_VALUE) {
                this.wait();
            }
            data.add(number);
            this.notifyAll();
            System.out.println("put one :" + number);

        }
    }

}


启动8个消费者和两个生产者,运行结果如下:


Thread[consumer:[0],5,main] : wait
Thread[consumer:[7],5,main] : wait
Thread[consumer:[6],5,main] : wait
Thread[consumer:[5],5,main] : wait
Thread[consumer:[4],5,main] : wait
Thread[consumer:[3],5,main] : wait
Thread[consumer:[2],5,main] : wait
Thread[consumer:[1],5,main] : wait
put one :1
Exception in thread "consumer:[3]" Exception in thread "consumer:[4]" Exception in thread "consumer:[6]" Exception in thread "consumer:[5]" Exception in thread "consumer:[0]" Exception in thread "consumer:[7]" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
put one :0
	at java.util.ArrayList.rangeCheck(ArrayList.java:657)
Thread[consumer:[1],5,main]-----> get one :1
	at java.util.ArrayList.get(ArrayList.java:433)
Thread[consumer:[2],5,main]-----> get one :0
	at com.nicky.spurious.SpuriousWakeupRWLock$CustomQueue.getOne(SpuriousWakeupRWLock.java:59)
	at com.nicky.spurious.SpuriousWakeupRWLock.lambda$main$0(SpuriousWakeupRWLock.java:20)
	at java.lang.Thread.run(Thread.java:748)
java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
	at java.util.ArrayList.rangeCheck(ArrayList.java:657)
	at java.util.ArrayList.get(ArrayList.java:433)
	at com.nicky.spurious.SpuriousWakeupRWLock$CustomQueue.getOne(SpuriousWakeupRWLock.java:59)
	at com.nicky.spurious.SpuriousWakeupRWLock.lambda$main$0(SpuriousWakeupRWLock.java:20)
	at java.lang.Thread.run(Thread.java:748)
java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
	at java.util.ArrayList.rangeCheck(ArrayList.java:657)
	at java.util.ArrayList.get(ArrayList.java:433)
	at com.nicky.spurious.SpuriousWakeupRWLock$CustomQueue.getOne(SpuriousWakeupRWLock.java:59)
	at com.nicky.spurious.SpuriousWakeupRWLock.lambda$main$0(SpuriousWakeupRWLock.java:20)
	at java.lang.Thread.run(Thread.java:748)

.............

3 唤醒异常原因

3.1 执行流程

线程在调用getOne方法的是时候,获取锁 –> 当前队列为空–> 进入wait状态 –> 释放锁,所有线程都进入等待状态, 这时候通过putOne方法往队列里面添加元素 –> 唤醒线程 –> 线程直接往下运行 –> 异常

3.2 异常原因

产生原因是在线程从进入wait状态时候的条件通过唤醒的时候,线程并未再次进入该条件:

if (data.size() <= 0) {
                System.out.println(Thread.currentThread() + " : wait");
                TimeUnit.SECONDS.sleep(1);
                this.wait();
            }


上述代码使用的是if条件语句会导致如果等待的线程被唤醒,不会再次执行if的条件,直接往下执行,从而破坏了线程竞争资源的 竞态条件问题

解决方案是将if条件语句修改为while语句,如下

while (data.size() <= 0) {
                System.out.println(Thread.currentThread() + " : wait");
                TimeUnit.SECONDS.sleep(1);
                this.wait();
            }

此时,如果多个线程被唤醒,则会重新调用while语句,从而再次竞争资源,结果如下:

Thread[consumer:[0],5,main] : wait
Thread[consumer:[7],5,main] : wait
Thread[consumer:[6],5,main] : wait
Thread[consumer:[5],5,main] : wait
Thread[consumer:[4],5,main] : wait
Thread[consumer:[3],5,main] : wait
Thread[consumer:[2],5,main] : wait
Thread[consumer:[1],5,main] : wait
put one :1
put one :0
Thread[consumer:[1],5,main]-----> get one :1
Thread[consumer:[2],5,main]-----> get one :0
Thread[consumer:[3],5,main] : wait
Thread[consumer:[4],5,main] : wait
Thread[consumer:[5],5,main] : wait
Thread[consumer:[6],5,main] : wait
Thread[consumer:[7],5,main] : wait
Thread[consumer:[0],5,main] : wait

JUC中的队列ArrayBlockingQueue就是使用的这种方式避免虚假唤醒,比如take方法


    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }
    }
    

文章目录
-----------------------
最新评论

[评论][COMMENTS]