简体   繁体   中英

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

I'm learning about synchronized code blocks and .wait() / .notify() methods in Java and having a hard time wrapping my head around how they interact in a producer-consumer setup. The same instance of the below class is passed to two threads; one thread runs the producer method and the other runs the consumer method.


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

        }
    }

}

Here, synchronized on the same object makes it so that only one of the two code-blocks runs at the same time. Let's say the producer thread wins the race, adds an element to the queue, and calls notify. At this point, the consumer thread will be waiting at synchronized(lock) in the consumer function (it never got to go into its code block because of sycnhornized ). As soon as the producer thread exits its synchronized code block, the consumer thread will enter its. Now, the queue is non-empty because the producer just put something in before notifying. Consumer thread will remove it, call notify, exits its block, at which point the producer will acquire the lock since it had now been waiting at synchronized(lock) line in the producer function. Three questions:

  1. It appears to me like we are alternating between producer and consumer, so that the queue size will oscillate between 0 and 1. What am I missing?

  2. Since exiting a synchronized code block frees the lock which the waiting thread can then see and acquire, why do we need the whole wait and notify mechanism? It seems to me like the notify in what I described above isn't doing anything, since as soon as the lock becomes available, the other thread acquires it and enters its code block.

  3. Does lock.notify() also wake up thread waiting at synchronized(lock) ?

Please check the whole documentation of notify and wait

You are seeing an example ofthread starvation .

One way for starvation to happen is if you write loops like this:

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

The problem is, the very next thing that the thread does after releasing lock is, it locks it again. If any other thread currently is blocked awaiting the same lock, then the thread that executes this loop is almost certain to win the race to lock it again because the thread that's executing the loop is already running, but the other thread needs time to "wake up."

We say that the other thread is "starved" in that case.

Some threading libraries offer a so-called fair lock, which avoids starvation by ensuring that the lock always will be awarded to whichever thread has been waiting the longest. But fair locks usually are not the default because they hurt the performance of better-designed programs in which locks are not so heavily contested.


In your example, the starvation is not a total disaster because each thread calls wait() when it runs out of work to do. That releases the lock and allows the other thread to run. But it pretty much forces the threads to "take turns:" One will always be sleeping while the other is working. You might just as well have written it as a single-threaded program.


It's better if your threads don't keep any lock locked longer than absolutely necessary:

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

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

Here I've moved the println(...) call out of the synchronized block. (I've also renamed your lock variable to emphasize that it's purpose specifically is to protect the queue.)

You could do the same in the producer thread by moving the random() call out of the synchronized block. In that way you get more opportunity for the two threads to run in parallel--the producer can be working to produce each new thing while the consumer is simultaneously dealing with some thing that it has "consumed."


Just to clarify: Here's what might actually happen:

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
         .                               .
         .                               .
         .                               .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM