在 Java 中强制进行虚假唤醒

2022-09-01 14:31:27

这个问题不是关于虚假唤醒是否真的发生,因为这里已经详细讨论了这个问题:Java中的虚假唤醒真的会发生吗?因此,这也不是关于,为什么我必须围绕我的声明进行循环。这是关于什么:wait

我想构建一个案例,其中虚假的唤醒发生。到目前为止,我在上面链接的问题中学到的是这样的:

如果一个Linux进程被发出信号,它的等待线程将各自享受一个漂亮的,热的虚假唤醒。

所以这似乎只适用于Linux机器,事实上我有Ubuntu 11.04 - 64-Bit。我编写了一个Java程序,其中一个线程等待条件,但没有循环和另一个线程等待并获得另一个线程通知的类。我以为在一个JVM中启动所有三个线程会强制上述情况,但似乎情况并非如此。

有没有人有其他想法如何在Java中构建这样的案例?


答案 1

你不能强制进行虚假唤醒,但对于正在运行的线程来说,虚假唤醒与常规唤醒没有区别(事件的来源不同,但事件本身是相同的)

要模拟虚假唤醒,只需调用 notify();

调用不合适,因为这样做会设置中断标志,并且在虚假唤醒后,不会设置中断标志interrupt()


答案 2

“虚假唤醒”是一个大杂烩,涵盖了该领域的任何实现细节。因此,很难弄清楚什么是“真实”的虚假唤醒,以及为什么另一个是“不真实的” - 更不用说这个实现细节起源于哪一层了。从“内核”,“系统库(libc)”,“JVM”,“Java标准库(rt.jar)”或在此堆栈之上构建的自定义框架中选择任何一个。

以下程序显示了使用内容的虚假唤醒:java.util.concurrent

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class SpuriousWakeupRWLock {
    static Lock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();
    static int itemsReady;

    public static void main(String[] args) throws Exception {

        // let consumer 1 enter condition wait
        new ConsumerOne().start();
        Thread.sleep(500);

        lock.lock();
        try {
            // let consumer 2 hit the lock
            new ConsumerTwo().start();
            Thread.sleep(500);

            // make condition true and signal one (!) consumer
            System.out.println("Producer: fill queue");
            itemsReady = 1;
            condition.signal();
            Thread.sleep(500);
        }
        finally {
            // release lock
            lock.unlock();
        } 

        System.out.println("Producer: released lock");
        Thread.sleep(500);
    }

    abstract static class AbstractConsumer extends Thread {
        @Override
        public void run() {
            lock.lock();
            try {
                consume();
            } catch(Exception e){
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
        abstract void consume() throws Exception;
    }

    static class ConsumerOne extends AbstractConsumer {
        @Override
        public void consume() throws InterruptedException {
            if( itemsReady <= 0 ){      // usually this is "while"
                System.out.println("One: Waiting...");
                condition.await();
                if( itemsReady <= 0 )
                    System.out.println("One: Spurious Wakeup! Condition NOT true!");
                else {
                    System.out.println("One: Wakeup! Let's work!");
                    --itemsReady;
                }
            }
        }
    }

    static class ConsumerTwo extends AbstractConsumer {
        @Override
        public void consume() {
            if( itemsReady <= 0 )
                System.out.println("Two: Got lock, but no work!");
            else {
                System.out.println("Two: Got lock and immediatly start working!");
                --itemsReady;
            }
        }
    }
}

输出:

One: Waiting...
Producer: fill queue
Producer: released lock
Two: Got lock and immediatly start working!
One: Spurious Wakeup! Condition NOT true!

使用的JDK是:

java version "1.6.0_20"
OpenJDK Runtime Environment (IcedTea6 1.9.9) (6b20-1.9.9-0ubuntu1~10.04.2)
OpenJDK 64-Bit Server VM (build 19.0-b09, mixed mode)

它基于以下中的一个实现细节:标准有一个等待队列,另一个等待队列。如果发出条件信号,则信令线程将从条件的队列移动到锁的队列中。实现细节:它被移动到队列的末尾。如果另一个线程已经在锁定队列中等待,并且第二个线程没有访问条件变量,则此线程可以“窃取”信号。如果实现将第一个线程放在第二个线程之前,则不会发生这种情况。这个“奖励”可以/将基于这样一个事实,即第一个线程已经获得了一次锁,并且与同一锁关联的条件下的等待时间记入该线程。java.util.concurrentLockCondition

我将其定义为“虚假的”,因为

  • 该病症仅发出一次信号,
  • 只有一个线程被该条件唤醒
  • 但是线程被条件唤醒发现它不是真的
  • 另一个线程从未触及条件,因此是“幸运但无辜的”
  • 稍微另一种实现可以防止这种情况。

最后一点通过此代码使用:Object.wait()

public class SpuriousWakeupObject {
    static Object lock = new Object();
    static int itemsReady;

    public static void main(String[] args) throws Exception {

        // let consumer 1 enter condition wait
        new ConsumerOne().start();
        Thread.sleep(500);

        // let consumer 2 hit the lock
        synchronized (lock) {
            new ConsumerTwo().start();
            Thread.sleep(500);

            // make condition true and signal one (!) consumer
            System.out.println("Producer: fill queue");
            itemsReady = 1;
            lock.notify();

            Thread.sleep(500);
        } // release lock
        System.out.println("Producer: released lock");
        Thread.sleep(500);
    }

    abstract static class AbstractConsumer extends Thread {
        @Override
        public void run() {
            try {
                synchronized(lock){
                    consume();
                }
            } catch(Exception e){
                e.printStackTrace();
            }
        }
        abstract void consume() throws Exception;
    }

    static class ConsumerOne extends AbstractConsumer {
        @Override
        public void consume() throws InterruptedException {
            if( itemsReady <= 0 ){      // usually this is "while"
                System.out.println("One: Waiting...");
                lock.wait();
                if( itemsReady <= 0 )
                    System.out.println("One: Spurious Wakeup! Condition NOT true!");
                else {
                    System.out.println("One: Wakeup! Let's work!");
                    --itemsReady;
                }
            }
        }
    }

    static class ConsumerTwo extends AbstractConsumer {
        @Override
        public void consume() {
            if( itemsReady <= 0 )
                System.out.println("Two: Got lock, but no work!");
            else {
                System.out.println("Two: Got lock and immediatly start working!");
                --itemsReady;
            }
        }
    }
}

输出:

One: Waiting...
Producer: fill queue
Producer: released lock
One: Wakeup! Let's work!
Two: Got lock, but no work!

在这里,实现似乎像我预期的那样:首先唤醒使用该条件的线程。

最后说明:这个原则的想法来自为什么java.util.concurrent.ArrayBlockingQueue使用“while”循环而不是“if”来调用wait()?尽管我的解释是不同的,代码来自我自己。