Synchronization of Threads

Introduction

It can be difficult to control threads when they need to share data between the threads and a common object. It is difficult to predict when data will be passed, often being slightly different each time the application runs. In certain situations it is vital that we synchronize threads to prevent this unpredictable behaviour and incorrect results.

A Good Bad Example

Suppose we have two gates into a stadium and that each time that a person goes through a gate a message is sent to a central counter to calculate the total number of people in the stadium. It would be vital for selling admittance to insure that the total number of people in the stadium is less than the total capacity of the stadium.

Each gate in this example is operating independently and so resembles a thread. They share the central counter, which in this case resembles a suitable object.

Figure 11.6, “A Non-Synchronized Gate Counter Example” shows my implementation of this example. The first two TextField components are the independent gate counters (counting the number of people arriving at the stadium). The third TextField object is a summation of the individual gates, so this is what the total should be. The "Actual Total" TextField is what our shared object (central counter) believes the total to be. As you can see there is a significant difference between what the total is, and what it should be. Why is this?

Well the reason is that the GateDetails "central counter" is not synchronized. When the first gate counter calls the spectatorEntered() method, it is entirely possible that the thread manager decides that that thread has had enough CPU cycles, and passes control to the second gate counter thread. You will notice that the first thread would have stored the current total in tempInt before it was stopped by the CPU, but the second thread would have updated the numberSpectators state while this thread was paused. When control is given back to the first thread, it would continue with the spectatorEntered() method, but it would be working with the old total, as stored in tempInt. I have purposly made it more difficult for the "central counter" to work correctly, by adding a sleep(5) call to force a delay of 5ms in each thread's counting cycle. See Figure 11.7, “A Non-Synchronized Gate Counter Example (Program Flow)” to see the program flow of the two threads. Remember that the two threads are sharing the same object.

Figure 11.6. A Non-Synchronized Gate Counter Example

A Non-Synchronized Gate Counter Example

The entire code for this non-synchronized example as shown in Figure 11.6, “A Non-Synchronized Gate Counter Example” is in GateCounter.java The code in this example is:

 1 
 2 
 3   class GateDetails
 4   {
 5 	private int numberSpectators;
 6 
 7 	public void spectatorEntered()
 8 	{
 9 		int tempInt = this.numberSpectators;
10 		try{ 
11 			//added to help cause problems.
12 			Thread.currentThread().sleep(5);  
13 		}
14 		catch (InterruptedException e) 
15 		{
16 			System.out.println(e.toString());
17 		}
18 		tempInt++;
19 		this.numberSpectators = tempInt;
20 	}
21 
22 	public int getTotalSpectators() { return numberSpectators; }
23   }
24 
25 

Figure 11.7. A Non-Synchronized Gate Counter Example (Program Flow)

A Non-Synchronized Gate Counter Example (Program Flow)

In this example of the problem of non-synchronized code I have made the problem worse, by inserting a quite unnecessary delay of 5ms. I did this to encourage the thread manager to change from execution of thread1 to thread2 and vice-versa. If this delay was not there the problem would not have been so pronounced, instead of almost 50% of the specators not being counted, maybe 1 in 1000 would not be counted, depending on your CPU conditions, the number of gates etc. The point is that it is unpredictable and should be fixed.

So how do we fix it? Well the answer is straightforward enough, we use the synchronized keyword, but the implementation is not quite as easy - when do we use it?

The synchronized keyword can be used to group a set of instructions that should not be interrupted by the thread manager, in other words a synchronized block of code should run to completion. In this example we can fix the GateDetails class to work correctly by adding the synchronized keyword to the spectatorEntered() method and to the getTotalSpectators() (for safety). Figure 11.8, “A Synchronized Gate Counter Example” shows a screen-capture of the applet running correctly and Figure 11.9, “A Synchronized Gate Counter Example (Program Flow)” displays the program flow in the situation where the code has been modified. Note that I have still left in the delay to prove that it works correctly.

Figure 11.8. A Synchronized Gate Counter Example

A Synchronized Gate Counter Example

The entire code for this synchronized example as shown in Figure 11.8, “A Synchronized Gate Counter Example” is in GateCounterFixed.java The fixed code in this example is:

 1 
 2 
 3   class GateDetails
 4   {
 5 	private int numberSpectators;
 6 
 7 	public synchronized void spectatorEntered()
 8 	{
 9 		int tempInt = this.numberSpectators;
10 		try{ 
11 		    //added to help cause problems.
12 			Thread.currentThread().sleep(5);  
13 		}
14 		catch (InterruptedException e) 
15 		{
16 			System.out.println(e.toString());
17 		}
18 		tempInt++;
19 		this.numberSpectators = tempInt;
20 	}
21 
22 	public synchronized int getTotalSpectators() 
23 	{ 
24 	  return numberSpectators; 
25 	}
26   }
27 
28 

As you can see in Figure 11.9, “A Synchronized Gate Counter Example (Program Flow)” the first thread executes as it has previously until it receives a request from the thread manager to transfer control to the next thread - but since the spectatorEntered() has been tagged with the synchronized keyword then this method must run to completion. So the second thread must wait until this method is finished before it can be loaded into the CPU and executed. Because of this, the total number of sepectators is incremented correctly to 501 before the second thread begins. This means that the second thread starts counting from 501 and correctly increments the total to 502.

Figure 11.9. A Synchronized Gate Counter Example (Program Flow)

A Synchronized Gate Counter Example (Program Flow)

Adding Synchronization to Code

We add synchronization to our code by either modifying the methods that we use to share this data like:

 1 
 2 
 3 	public synchronized void theSynchronizedMethod()
 4 	{
 5 	
 6 	}
 7 	
 8 

Or we could select a block of code to synchronize and use:

 1 
 2 
 3 	synchronized(anObject)
 4 	{
 5 
 6 	}
 7 
 8 

This works like a lock on objects. When two threads execute code on the same object, only one of them acquires the lock and proceeds. The second thread waits until the lock is released on the object. This allows the first thread to operate on the object, without any interruption by the second thread.

Again, synchronization is based on objects:

  • Two threads call synchronized methods on different objects, they proceed concurrently.

  • Two threads call different synchronized methods on the same object, they are synchronized.

  • Two threads call synchronized and non-synchronized methods on the same object, they proceed concurrently.

Static methods are synchronized per class. The standard classes are multithread safe.

It may seem that an obvious solution would be to synchronize everything!! However this is not that good an idea as when we write an application, we wish to make it:

  • Safe - We get the correct results.

  • Lively - It performs efficiently, using threads to achieve this liveliness.

These are conflicting goals, as too much synchronization causes the program to execute sequentially, but synchronization is required for safety when sharing objects. Always remove synchronization if you know it is safe, but if you are not sure then synchronize.