LAB 13 - Experiencing Race Conditions with Java Threads
Goal
- Experiencing Race Conditions running 2 Threads associated to IntensiveSumRC. The correct result is:
sum = 2158509704
sum = 2670668000
- Understanding why race condition occurs when using IntensiveSumRC.java
- Write an IntensiveSumNoRC for avoiding race condition, using synchronized keyword
- Check wheter the results are correct this time
- Write an IntensiveSumNoRC for avoiding race condition, using ReentrantLock
- Check wheter the results are correct
Race Conditions
A race condition is a situation in which two or more threads or processes are reading or writing some shared data, and the final result depends on the timing of how the threads are scheduled. Race conditions can lead to unpredictable results and subtle program bugs. A thread can prevent this from happening by locking an object. When an object is locked by one thread and another thread tries to call a synchronized method on the same object, the second thread will block until the object is unlocked.
Race Condition in IntensiveSumRC
If two threads execute n=n+1 on a shared variable n at about the same time,
their load and store instructions might interleave so that one thread
overwrites the update of the other.
This lost update, when it occurs, leads to an erroneous result
and is an example of a race condition. Race conditions are
possible when two or more threads share data, they are reading
and writing the shared data concurrently, and the final result
of the computation can vary, depending on which thread does what when.
Some of the possible final results are correct and some are incorrect.
For example, two threads share the variable a whose initial value is 0.
One thread performs the assignment a=1 and another thread performs at
about the same time the assignment a=2. The final result, 1 or 2, is
a race condition. Depending on the application, both might be correct.
However, suppose one thread performs the update a=a+1 and another
thread performs at about the same time the update a=a+2. The final result
is a race condition and could be 1, 2, or 3, only one of which is correct.
Synchronized Blocks
Every Java object has a lock. A synchronized block uses an object's lock to act like a binary semaphore having initial value one. It solves the mutual exclusion critical section problem:
Object obj = new Object();
...
synchronized (obj) { // in a method
... // any code, e.g., critical section
}
The synchronized method construct
... synchronized method(...) {
... // body of method
}
is an abbreviation for
... method(...) {
synchronized (this) {
... // body of method
}
}
that is, the entire body of the instance method is a synchronized block on the object (keyword this) the method is in.
Locks
Package java.util.concurrent.locks contains an interface Lock and a class ReentrantLock that implements the Lock interface. Locks can be used in place of synchronized blocks and to implement synchronized methods, that is, use:
class ... {
Lock mutex = new ReentrantLock();
...
... method(...) {
mutex.lock();
try {
... // code that might return or throw an exception
} finally {
mutex.unlock();
}
}
}
in place of
class ... {
Object mutex = new Object();
...
... method(...) {
synchronized (mutex) {
... // code that might return or throw an exception
}
}
}
or
class ... {
...
... method(...) {
synchronized (this) {
... // code that might return or throw an exception
}
}
}
Locks offer more flexibility than synchronized blocks in that a thread can unlock multiple locks it holds in a different order than the locks were obtained. This cannot be done with the implied locks of synchronized blocks because synchronized blocks must be lexically nested.