在 Java 中使用带有 .wait 和 .notify 的 synchronized 代码块

Using `synchronized` code blocks with `.wait` and `.notify` in Java

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


    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) 等待的线程?

请检查 notify and wait

的整个文档

您看到的是 thread starvation 的示例。

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

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

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

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

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


在您的示例中,饥饿并不是一场彻底的灾难,因为每个线程在 运行 无事可做时调用 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
         .                               .
         .                               .
         .                               .