"BufferExample" shows basic usage of "sychronized", "wait" and "notify".

The example creates a 'consumer' thread which interacts with the main thread
through a shared instance of "Buffer".  The main thread places the numbers
1..7 into the buffer.  The consumer extracts them and prints them.

In the Buffer class, notice how all of the methods are defined to be 
synchronized.  This means that, when those methods are invoked, the calling
thread has to acquire a mutual exclusion lock associated with the buffer
object.  The lock does not prevent external access to the fields of the buffer:
we need to make the fields "private" in order to achieve that.

The invocations of "wait" cause the calling thread to atomically (i)
release the lock that it holds on the buffer object and (ii) block waiting
until it receives a notification.  The invocations of "notifyAll" cause
all of the threads waiting for notification on that object to be released.
Notice how they must then compete to re-acquire the mutual exclusion lock
associated with the object -- they can't proceed right away.

The "Buffer" class illustrates the usual way in which these facilities are
used: (i) mark all methods synchronized, (ii) make all fields private, 
(iii) at the start of each method loop calling wait() until some kind of
entry condition is satisfied (e.g. the buffer is empty) and then (iv) at
the end of each method call notifyAll() so that any waiting threads can
re-evaluate their conditions. 

