1

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

 2 years ago
source link: https://nicky-chin.cn/2020/04/23/spurious-wakeup/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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();
        }
    }
    


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK