Synchronization of Threads
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 ensure 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 8.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
TextFieldobject is the sum 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 StadiumDetails "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 purposely 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 8.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 StadiumDetails object.
Figure 8.6. A Non-Synchronized Gate Counter Example
The entire code for this non-synchronized example as shown in Figure 8.6, “A Non-Synchronized Gate Counter Example” is in
GateCounterApp.java The code segment for this example is:
Figure 8.7. 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-verse. If this delay was not there the problem would not have been so pronounced, instead of almost 50% of the spectators 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?
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
StadiumDetails class to work correctly by adding the
synchronized keyword to the
spectatorEntered() method and to the
getTotalSpectators() (for safety). Figure 8.8, “A Synchronized Gate Counter Example” shows a screen-capture of the application running correctly and Figure 8.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 8.8. A Synchronized Gate Counter Example
The entire code for this synchronized example as shown in Figure 8.8, “A Synchronized Gate Counter Example” is in
GateCounterAppSync.java The fixed code in this example is:
As you can see in Figure 8.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 spectators 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 8.9. 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:
3 public synchronized void theSynchronizedMethod()
Or we could select a block of code to synchronize and use:
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.