【问题标题】:Using `synchronized` code blocks with `.wait` and `.notify` in Java在 Java 中将 `synchronized` 代码块与 `.wait` 和 `.notify` 一起使用
【发布时间】:2021-01-08 12:08:38
【问题描述】:

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

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

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

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

【问题讨论】:

    标签: java multithreading concurrency synchronized producer-consumer


    【解决方案1】:

    请查看notifywait的完整文档

    【讨论】:

    • 是的,我在发布后不久就在我的问题中解决了这个问题,但你在我保存之前回复了:)
    • 你能回答我的主要问题吗?
    【解决方案2】:

    您看到的是 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
             .                               .
             .                               .
             .                               .
    

    【讨论】:

    • 1.不需要唤醒等待进入同步代码块的线程是否正确(当锁可用并尝试获取它时它将自动唤醒),而通过 .wait() 等待的线程确实如此需要叫醒吗?
    • 2.您提到由于 while(true),锁定切换实际上仅在生产者或消费者耗尽工作时才会发生 - 即在 .wait() 处。现在,如果另一个线程在 synchronized(lock) 处一直在等待,它将自动唤醒并获取锁。但如果它一直在等待(),是什么唤醒了它?自上次 .notify() 以来它不会一直处于休眠状态吗? (在最后的 .notify() 中,它会醒来,但随后输给了第一个线程并重新进入睡眠状态)
    • 你能回答一下吗? :')
    • @ion20,回复 (1),是的。这是正确的。只要任何线程离开synchronized 块,它就会自动使其可供其他线程进入。但是,如果线程正在等待,Java 语言规范没有说明 哪个 其他线程将被允许进入。如果您的应用程序需要“公平”锁定,您可以使用 ReentrantLock 类而不是使用 @ 987654333@ 块。
    • @ion20, Re (2), 这不是while(true),而是一个线程在离开synchronized 块后的下一件事情,它在循环的顶部再次进入块。这使它在另一个线程上“领先”,在等待进入时可能被“换出”。 “生产者”线程的整个主体和“消费者”线程的整个主体是同步的,因此它们不可能并行运行。由于饥饿问题,一个人将控制权交给另一个人的唯一真正机会是其中一个人致电wait()
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-03-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-05-01
    • 2020-06-09
    • 1970-01-01
    相关资源
    最近更新 更多