Session 12 |
Thread synchronization
To avoid race conditions, it is necessary to prevent multiple threads entering a certain part of the program (critical region) simultaneously. The critical region in previous example (class Counter
) is the entire increment
method. We can use the keyword synchronized
to synchronize the method so that only one thread can access the method at a time. There are several ways to correct the problem in previous example (class Counter
). One approach is to make Counter
thread-safe by adding the keyword synchronized
in the increment
method in line 4, as follows:
public synchronized void increment() {
A synchronized method is assigned a lock when it is executed by a thread. A lock is a mechanism for exclusive use of the resource. In the case of a static method, the lock is assigned to the class when the method is invoked. In case of an instance method, the lock is assigned to the the method when the method is invoked. If one thread invokes a synchronized instance method (respectively, a static method), the lock is assigned first, then the method is executed, and finally the lock is released. If another thread invokes the locked instance method (respectively, class), it has to wait until the lock is released.
Coming back to the example, the scenario of the synchronized increment
method is the following. If Task 1 enters the method, Task 2 is blocked until Task 1 finishes the method, as shown in the figure below.
A synchronized statement can be used to acquire a lock on any object, not just this object, when executing a block of the code in a method. This block is referred to as a synchronized block. A general form of the synchronized statement is as follows:
synchronized (expr) { statements; }
The expression expr
must evaluate the object reference. If the object is already locked by
another thread, the thread is blocked until the lock is released. When a lock is obtained on the
object, the statements in the synchronized block are executed and then the lock is released.
Synchronized statements enable to synchronize part of the code in a method instead
of the entire method. This increases concurrency. We can make class Counter
thread-safe by
placing the statement in line 25 inside a synchronized block:
synchronized (counter) { counter.increment(); }
Let us now develop our counter example further. Suppose we want to add one more method (in addition to the increment
method) which also uses the field n
. In the class, there are a few critical regions where we want to have only one thread at a time. Both critical sections use the same monitor, which is a completely separate object in this case.
public class Counter { private Object monitor = new Object(); private int n = 0; public void increment() { synchronized (monitor) { n = n + 1; } } public int getN() { synchronized (monitor) { return n; } } }
If the first thread enters the critical region of the increment
method, it will get permission from the monitor. If another thread attempts to use the critical region of the increment
method before the first thread finishes its work, the monitor will force the second thread to wait until the first thread has completed its work (the second thread will wait at the line with the synchronized
key). Since the same monitor is used in the critical region of the getN
method, the second thread would not be able to use this method without waiting for it.
Note that all sections of the code that use the protected data, including those that only read data, should be indicated as critical regions. In this way, we avoid situations in which semi-state data are read. Detection of critical regions is much easier when the protected data are in a private field. When the protected data are shared between objects, we need to make sure that all objects use the same monitor (it's difficult; try to avoid sharing the data).
Session 12 |