This example is another one built over counting semaphores.  However, its purpose is to
illustrate how individual mutexes and condition variables can be used rather than to
illustrate the semaphores themselves.

"Mutex" and "CondVar" build mutexes and condition variables using the "CountingSemaphore"
class, following the design presented in the lecture notes.  For example, a "Mutex"
is implemented using a single counting semaphore which is initialized to the value 1 
and is locked by performing a "P" operation on the semaphore and unlocked by performing
a "V" operation.

The "Buffer" interface defines a simple single-cell shared buffer holding integer 
values.

"BufferBasic" implements the buffer using one mutex and one condition variable.  The
implementation therefore follows the usual Java one very closely, except that the
mutex and condition variable are accessed explicitly through fields, rather than using
the alternative built-in concurrency control primitives associated with Java objects.

"BufferMultipleCondvars" shows an alternative implementation in which two condition
variables are used.  One of these is used to represent the 'buffer full' condition and
the other is used to represent the 'buffer empty' condition.  Although this makes the
buffer's implementation more complicated, it means that notifications can be directed
more precisely.  For example, when a "put" operation has succeeded, only the threads
blocked on the 'buffer empty' condition can be notified.  This technique can be useful
in more complex data structures, or when there are a lot of threads, because it reduces
the number of threads which are needlessly woken.

"BufferExample1" runs the example using the basic buffer implementation.

"BufferExample2" runs the example using the multiple-condvars implementation.