在 Java 中使用带有“.wait”和“.notify”的“同步”代码块

问题描述

我正在学习 Java 中的 synchronized 代码块和 .wait()/.notify() 方法,但很难理解它们在生产者-消费者设置中的交互方式。下面类的同一个实例被传递给两个线程;一个线程运行生产者方法,另一个运行消费者方法


    private Queue<Integer> queue = new LinkedList<>();
    private Object lock = new Object();

    public void producer() throws InterruptedException {
        Random random = new Random();
        while (true) {

            synchronized(lock) {
                while (queue.size() > 10) {
                    lock.wait();
                }

                queue.add(random.nextInt(100));
                lock.notify();
            }

        }
    }

    public void consumer() throws InterruptedException {
        while (true) {

            synchronized(lock) {
                if (queue.isEmpty()) {
                    lock.wait();
                }

                int val = queue.remove();
                System.out.println(val + ": " + queue.size());
                lock.notify();
            }

        }
    }

}

在这里,同一对象上的 synchronized 使得 只有 两个代码块之一同时运行。假设生产者线程赢得比赛,向队列添加一个元素,并调用通知。此时,消费者线程将在消费者函数中的 synchronized(lock) 处等待(由于 sycnhornized,它永远不会进入其代码块)。一旦生产者线程退出它的同步代码块,消费者线程就会进入它的。现在,队列是非空的,因为生产者只是在通知之前放入了一些东西。消费者线程将删除它,调用通知退出它的块,此时生产者将获取锁,因为它现在已经在生产者函数中的 synchronized(lock) 行等待。三个问题:

  1. 在我看来,我们在生产者和消费者之间交替,因此队列大小会在 0 和 1 之间波动。我错过了什么?

  2. 既然退出同步代码块会释放等待线程可以看到和获取的锁,为什么我们需要整个等待和通知机制?在我看来,我上面描述的 notify 没有做任何事情,因为一旦锁可用,另一个线程就会获取它并进入它的代码块。

  3. lock.notify() 是否也会唤醒在 synchronized(lock) 处等待的线程?

解决方法

请查看 notifywait

的整个文档 ,

您正在看到 thread starvation 的示例。

饥饿发生的一种方式是你写这样的循环:

while (true) {
    synchronized(lock) {
        ...
    }
}

问题是,线程在释放 lock 后所做的下一件事是,它再次锁定它。如果当前有任何其他线程被阻塞等待同一个锁,那么执行此循环的线程几乎肯定会赢得再次锁定它的竞争,因为执行循环的线程已经在运行,但其他线程需要时间来“唤醒”。

在这种情况下,我们说另一个线程“饿死了”。

一些线程库提供所谓的公平锁,通过确保锁总是授予等待时间最长的线程来避免饥饿。但公平锁通常不是默认设置,因为它们会损害设计更好的程序的性能,在这些程序中,锁不会受到如此严重的竞争。


在您的示例中,饥饿并不是一场彻底的灾难,因为每个线程在没有工作要做时都会调用 wait()。这会释放锁并允许其他线程运行。但它几乎迫使线程“轮流”:一个将始终处于睡眠状态,而另一个正在工作。你也可以把它写成一个单线程程序。


如果您的线程保持锁定的时间不超过绝对必要的时间会更好:

while (true) {
    int val;
    synchronized(queue_lock) {
        if (queue.isEmpty()) {
            lock.wait();
        }

        val = queue.remove();
        queue_lock.notify();
    }
    System.out.println(val + ": " + queue.size());
}

这里我已将 println(...) 调用移出同步块。 (我还重命名了您的 lock 变量,以强调它的目的是保护队列。)

您可以通过将 random() 调用移出同步块,在生产者线程中执行相同的操作。这样,您就有更多机会让两个线程并行运行——生产者可以生产每个新事物,而消费者同时处理它“消耗”的某些事物。


澄清一下:以下是实际可能发生的情况:

producer                              consumer
---------------------------------     -----------------------------------
                                      enter synchronized block
tries to enter synchronized block     queue.isEmpty() => true
                                      lock.wait()
                                          ...releases the lock...
enters synchronized block                 ...awaiting notification...
queue.add(...)                            ...awaiting notification...
lock.notify()                             ...now awaiting the lock...
leave synchronized block                  ...starts to wake up,but...
enter synchronized block                  ...Dang! Lost the race...
queue.add(...)                            ...awaiting the lock...
lock.notify()
leave synchronized block                  ...starts to wake up,but...
enter synchronized block                  ...Dang! Lost the race...
    .                                     ...awaiting the lock...
    .                                          .
    .                                          .
queueSize() > 10                               .
lock.wait()
    ...releases the lock...               ...starts to wake up,and...
    ...awaiting notification...           ...FINALLY! re-acquire the lock,and...
         .                             lock.wait() returns
         .                             val = queue.remove()
         .                             ...
    ...now awaiting the lock...        lock.notify()
    ...starts to wake up,but...       leave synchronized block
    ...Dang! Lost the race...          enter synchronized block
         .                               .
         .                               .
         .                               .