Synchronizing Threads with Java Monitors

  1. Synchronized Java methods - exercise #1
  2. Thread coordination in Java - exercise #2
  3. Summary of synchronization mechanisms in Java
  4. Java Semaphores? - exercise #3


Review the tutorial on Java threads.

Synchronized Java methods

Java associates a monitor with each object. The monitor enforces mutual exclusive access to synchronized methods invoked on the associated object. When a thread calls a synchronized method on an object, the JVM ckecks the monitor for that object When a thread exits a synchronized method, it releases the monitor, allowing a waiting thread (if any) to proceed with its synchronized method call. Here is an example that demonstrates how Java monitors work.
      class Counter
      {
            private int count = 0;
            public void Increment() {
                int n = count;
                count = n+1;
            }
      }
What might happen if two threads share an object
      Counter counter;
and both try to execute
      counter.Increment();
almost simultaneously? One possible execution scenario (discussed in class):

Thread 1Thread 2
Count
counter.Increment();
---
0
n = count;     // 0
---
0
---
counter.Increment();
0
---
n = count;      // 0
0
---
count = n + 1;  // 1
1
count = n + 1; // 1
---
1

We can avoid this situation by declaring the Increment method to be synchronized, as in

      class Counter
      {
            private int count = 0;
            public void synchronized Increment() {
                int n = count;
                count = n+1;
            }
      }
The revised scenario:

Thread 1Thread 2
Count
counter.Increment();
---
0
(acquires the monitor)
---
0
n = count;     // 0
---
0
---
counter.Increment();
0
---
(can't acquire monitor)
0
count = n + 1; // 1 ---(blocked)
1
(releases the monitor) ---(blocked)
1
---
(acquires the monitor)
1
---
n = count;      // 1
1
---
count = n + 1;  // 2
2
---
(releases the monitor)
2

When Thread 2 attempts to execute the Increment() method on the same counter object, the thread is blocked. Thread 2 is unable to acquire ownership of the counter object's monitor; the monitor is already owned by Thread 1. Thread 2 is suspended until the monitor becomes available. When Thread 1 releases the monitor, Thread 2 is able to acquire the monitor and continue running, completing its call to the method.

Note that Java monitors are not like traditional critical sections. Declaring a method synchronized does not imply that only one thread may execute that method at a time, as would be the case with a critical section. It implies that only one thread may invoke that method (or any synchronized method) on a particular object at any given time. Java monitors are associated with objects, not with blocks of code. Two threads may concurrently execute the same synchronized method, provided that the method is invoked on different objects (that is, a.method() and b.method(), where a != b).

Exercise 1. Download the code PC.java for the producer-consumer problem. The code uses synchronized Get and Put access methods to a shared buffer. Compile and run the code. What happened? Why does this happen?

Now you'll need to kill the running process. Open a new telnet window and type in the following commands at the shell prompt:


      bash$ ps -ef | grep your_account_name
      bash$ kill -9 pid
where your_account_name is your login name. The first command lists all processes running on the Unix machine that you have created. Identify the PC process and use its identification number pid in the kill command.


Thread coordination in Java

In the producer-consumer code in exercise 1, the producer thread quickly fills the buffer with characters and then waits for the consumer to consume some characters from the buffer. The problem is that the producer waits inside the monitor associated with the buffer, preventing the consumer to execute the synchronized Get method on the buffer.

We really want the Producer to release the monitor if the buffer becomes full and allow the Consumer to proceed. Similarly, the Consumer must release the monitor if the buffer becomes empty and allow the Producer to proceed. To coordinate the two threads, we must use the Object's wait() and notify or notifyAll() methods.

The wait() method suspends the calling thread and temporarily releases ownership of the monitor (so it allows other threads to acquire the monitor). The suspended thread that called wait() wakes up only when another thread calls notify() or notifyAll() on that object.

The notifyAll() method wakes up all threads waiting on the object in question. The awakened threads compete for the monitor. One thread gets it, and the others go back to waiting.

The notify() method arbitrarily wakes up one of the threads waiting on the object in question. A correct implementation of the Buffer for the producer-consumer problem is given below.

      class Buffer {
            private char [] buffer;
            private int count = 0, in = 0, out = 0;

            Buffer(int size)
            {
                 buffer = new char[size];
            }
 
            public synchronized void Put(char c) {
                 while(count == buffer.length) 
                 {
                      try { wait(); }
                      catch (InterruptedException e) { } 
                      finally { } 
                 } 
                 System.out.println("Producing " + c + " ...");
                 buffer[in] = c; 
                 in = (in + 1) % buffer.length; 
                 count++; 
                 notify(); 
            }
    
            public synchronized char Get() {
                 while (count == 0) 
                 {
                      try { wait(); }
                      catch (InterruptedException e) { } 
                      finally { } 
                 } 
                 char c = buffer[out]; 
                 out = (out + 1) % buffer.length;
                 count--;
                 System.out.println("Consuming " + c + " ..."); 
                 notify(); 
                 return c;
            }
      }

Important note: A thread may call wait(), notify() or notifyAll() on an object, only if it owns the monitor of that object.

To summarize:

  1. If a class has one ore more synchronized methods, each object of the class gets a queue that holds all threads waiting to execute one of theng the synchronized methods.

  2. There two ways for a thread to get onto this queue, either by calling the method while another thread is using the object or by calling wait(), while using the object.

  3. When a synchronized method call returns, or when a method calls wait(), another thread gets access to the object.

  4. If a thread was put in the queue by a call to wait(), it must be "unfrozen" by a call to notify() before it can be scheduled for execution again.
The synchronization rules are complex, but it is actually quite simple to put them into practice. Just follow these rules:
  1. If two or more threads modify an object, declare the methods that carry out the modification as synchronized.

  2. If a thread must wait for the state of an object to change, it should wait inside the object, not outside, by entering the synchronized method and calling wait().

  3. Whenever a method changes the state of an object, it should call notify(). That gives the waiting threads a chance to see if circumstances have changed.
These concepts are emphasized in this advanced threads tutorial, take a look in case things are still blurry for you.

Exercise 2. Make the changes outlined above to the producer-consumer code from exercise 2 and recompile it. It should run successfully this time.


Java semaphores?

Java offers a counting Semaphore object that simplifies thread synchronization.

Exercise 5. Study the Java API specifications for semaphore objects and rewrite the producer-consumer code (PC.java) using semaphores this time.