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.
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.
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
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.
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.
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.
© 2006
Dr. Derek Molloy
(DCU).