Producer consumer water cooler example
Producer and Consumer


You properly synchronize on the sharedList.
Both Producer and Consumer fail to use the proper wait() idiom to guard against spurious wake up.

Producer waits until the list is empty :

while(!sharedList.isEmpty())
    sharedList.wait();


Consumer waits until the list is not empty :

while(sharedList.isEmpty())
    sharedList.wait();

Given that both wait on the same monitor with a different condition you must use notifyAll() instead of notify() :


  Single notify can be used instead of notifyAllonly when both of the
  following conditions hold:
  
  
  Uniform waiters. Only one condition predicate is associated with the condition queue, and each thread executes the same logic upon
  re-turning from wait
  One-in, one-out. A notification on the condition variable enables at most one thread to proceed.
  


(from Java Concurrency in Practice)
You set the threads in motion, but you have no way of stopping them. Consider using interruption for this.

Check the interrupted flag in your loop condition :

while (!Thread.currentThread().isInterrupted())

Properly deal with InterruptedException by restoring the interrupted state so the interruption is not lost.

Call Thread.interrupt() in the catch block :

} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

I assume this is some sort of exercise. Typically to connect producers and consumers you would use a blocking bounded queue.


Latstly : while println() gives you some nice output, it can give you a false sense of thread safety, as it does synchronisation under the hood, and in doing so introduces memory boundaries that may hide problems with your code, that will surface once you substitute them with actual production code.

WaterCooler


There is no reason to make list volatile. It is never reassigned. In fact it would be better to mark it final, to make sure this never happens.
Creating a thread, and starting it, gives you little control over it. (how will you stop them?)