Classic Synchronization #
In this section we discuss traditional synchronization primitives that have been provided in Java from the beginning.
Thread Interference #
Before discussing synchronization, we look at what can happen if different threads lack coordination. We will use a shared mutable object of the following class to demonstrate thread interference.
class Count {
public int count = 0;
}
The following test creates two threads
that manipulate a Count
instance.
void testThreadInterference() {
final Count count = new Count();
final Runnable inc = () -> {
final int val = count.value;
randomSleep();
count.value = val + 1;
};
final Runnable dec = () -> {
final int val = count.value;
randomSleep();
count.value = val - 1;
};
final IntSupplier race = () -> {
final Thread incThread = new Thread(inc);
final Thread decThread = new Thread(dec);
incThread.start();
decThread.start();
try {
incThread.join();
decThread.join();
} catch (InterruptedException e) {}
return count.value;
};
assertTrue(IntStream.generate(race).limit(100).anyMatch(n -> n != 0));
}
The definitions of inc
and dec
to increment and decrement the shared count
are only correct,
if the count is not changed by another thread during the randomSleep
.
However, when executing the race
many times,
it is highly likely that the count is manipulated by the other thread
at least in some executions.
Intrinsic Locks #
In Java, every object has a built-in lock.
We can modify the definitions of inc
and dec
to use the synchronized
keyword as follows.
final Runnable inc = () -> {
synchronized (count) {
final int val = count.value;
randomSleep();
count.value = val + 1;
}
};
final Runnable dec = () -> {
synchronized (count) {
final int val = count.value;
randomSleep();
count.value = val - 1;
}
};
When executing the synchronized block, no other thread can enter a block synchronized on the same object. The behaviour of the intrinsic lock is re-entrant, meaning that a thread that is already holding a lock of an object can enter other blocks synchronized on the same object.
Now, the assertion in the end can be rewritten to demonstrate that no thread interferes with the other between reading and writing the count value.
assertTrue(IntStream.generate(race).limit(100).allMatch(n -> n == 0));
The synchronized
keyword can also be used in method signatures,
meaning that the whole method definition is wrapped in a block
synchronized on the object the method is called on.
The FractalApp
uses intrinsic locks
to synchronize read and write access to a buffered image.
Interested readers are encouraged to look for the
synchronized
keyword in the ImageCanvas
class.
Releasing locks #
Locks are released when a thread exits a synchronized block.
Alternatively, threads can release a lock they are holding
by calling wait
on the corresponding object.
The wait
method blocks until another thread notifies
waiting threads, usually with notifyAll
.
The following test demonstrates how threads can wait for
and notify each other.
void testIntrinsicLockRelease() {
final Count count = new Count();
final Runnable consume = () -> {
synchronized (count) {
while (count.value == 0) {
try {
count.wait();
} catch (InterruptedException e) {}
}
count.value = 0;
count.notifyAll();
}
};
final Runnable produce = () -> {
synchronized (count) {
while (count.value != 0) {
try {
count.wait();
} catch (InterruptedException e) {}
}
count.value = 10;
count.notifyAll();
}
};
final IntSupplier handshake = () -> {
final Thread consumer = new Thread(consume);
final Thread producer = new Thread(produce);
consumer.start();
producer.start();
try {
consumer.join();
producer.join();
} catch (InterruptedException e) {}
return count.value;
};
assertTrue(IntStream.generate(handshake).limit(100).allMatch(n -> n == 0));
}
The consumer waits as long as the count value is zero, and the producer waits as long as it is non-zero. Regardless of which thread enters the synchronized block first, the producer will be the first to exit its waiting loop, write a non-zero count value, and notify the consumer, which will then continue to reset the count value to zero again.
Waiting threads should not assume that the condition they are waiting for is satisfied when they are notified. It is common practice to test it in a loop.