Java’s built-in support for threads is a double-edged sword. While it simplifies the development of concurrent applications by providing language and library support and a formal cross-platform memory model (it’s this model that makes write-once, run-anywhere concurrent applications possible), it also raises the bar for developers, since more programs will use threads. When threads were more esoteric, concurrency was an “advanced” topic; now, mainstream developers must be aware of thread-safety issues.
1. Safety hazards
Thread safety can go unnoticed, because in the absence of sufficient synchronization, the ordering of operations in multiple threads is unpredictable. Take a look at the program below, which behaves correctly in a single-threaded environment, but not in a multithreaded one.
@NotThreadSafepublic class UnsafeSequence { private int value;
/** returns a unique value **/ public int getNext() { return value++; }}The problem is that, with some unlucky timing, two threads can call getNext and receive the
same value. The increment value++ is actually a combination of 3 operations — read the value,
add 1 to it, write out the new value. Since operations across multiple threads may be arbitrarily
interwoven by the runtime, two threads can read the same value at the same time, both see the
same value, and both add 1 to it.
This is a common concurrency hazard called a race condition. Threads accessing/modifying variables that other threads use is a huge advantage, but it also means those same threads can be confused by data changing unexpectedly. Java provides synchronization mechanisms to coordinate such access.
The program above can be fixed by making getNext a synchronized method, preventing the
unfortunate interaction.
@ThreadSafepublic class Sequence { @GuardedBy("this") private int nextValue;
public synchronized int getNext() { return nextValue++; }}2. Liveness hazards
This occurs when an operation gets into a state where it’s unable to make forward progress. In a single thread, an example is an infinite loop, where the code following it never executes. In multithreaded apps, an example is when Thread A is waiting for a resource being used by Thread B — if B never releases it, A waits forever. Examples of liveness hazards include deadlocks, starvation, and livelock.
3. Performance hazards
In well-designed concurrent applications, using threads is a net performance gain, but threads still carry some runtime overhead. Context switches — when the scheduler suspends the active thread temporarily so another thread can run — are more frequent in applications with many threads, and have real costs: saving/restoring execution context, loss of locality, and CPU time spent scheduling instead of running. When threads share data, they must use synchronization mechanisms that can inhibit compiler optimizations, flush or invalidate memory caches, and create synchronization traffic on the shared memory bus. All of this introduces additional performance costs.
Thread safety
This is about protecting data from uncontrolled concurrent access. Whether an object needs to be thread-safe depends on whether it will be accessed from multiple threads. Making an object thread-safe requires using synchronization to coordinate access to its mutable state — failing to do so can result in data corruption.
Whenever more than one thread accesses a given state variable, and one of them might write to it,
they must all coordinate their access using synchronization. The primary mechanism for
synchronization in Java is the synchronized keyword, which provides exclusive locking, but
“synchronization” also covers volatile variables, explicit locks, and atomic variables.
If multiple threads access the same mutable state variable without appropriate synchronization, your program is broken. There are three ways to fix it:
- Don’t share the state variable across threads.
- Make the state variable immutable.
- Use synchronization whenever accessing the state variable.
When designing thread-safe classes, good object-oriented techniques — encapsulation, immutability, and clear specification of invariants — are your best friends.
What exactly is thread safety? A class is thread-safe if it behaves correctly when accessed from multiple threads, regardless of the scheduling or interleaving of the execution of those threads by the runtime, and with no additional synchronization or other coordination on the part of the calling code.
@ThreadSafepublic class StatelessFactorizer implements Servlet { public void service(Request req, Response res) { BigInteger i = extractFromRequest(req); BigInteger[] factors = factor(i); encodeIntoResponse(res, factors); }}The class above is stateless — it holds no data in itself, has no fields, and doesn’t reference any fields from other classes. Stateless objects are always thread-safe.
What happens if we add one element of state to what was once a stateless object? Say we want a
variable that increments each time a request is processed — we can add a long field and
increment it on each request.
@NotThreadSafepublic class UnsafeCountingFactorizer implements Servlet { private long count = 0; public long getCount() { return count; }
public void service(ServletRequest req, ServletResponse resp) { BigInteger i = extractFromRequest(req); BigInteger[] factors = factor(i); ++count; encodeIntoResponse(resp, factors); }}UnsafeCountingFactorizer is not thread-safe, even though it works fine single-threaded. While
++count looks like a single action due to its compact syntax, it’s not atomic — it doesn’t
execute as one indivisible operation. It’s shorthand for three discrete operations: fetch the
current value, add one, write the new value back. This is a read-modify-write operation, where
the resulting state derives from the previous state.
Why is the class above unsafe? If two threads try to increment the counter simultaneously without synchronization, and the counter is initially 9, with unlucky timing each thread could read the value, see 9, add one, and each set the counter to 10. An increment gets lost, and the counter is permanently off by one.
A slightly inaccurate hit count on a web service might be an acceptable loss of accuracy — but if the counter generates sequences or unique object identifiers, returning the same value from multiple invocations could cause serious data integrity problems. This possibility — incorrect results due to unlucky timing — is important enough in concurrent programming to have a name: a race condition.
Race conditions
UnsafeCountingFactorizer has several race conditions that make its result unreliable. A race
condition occurs when the correctness of a computation depends on the relative timing or
interleaving of multiple threads by the runtime — when getting the right answer relies on lucky
timing. The most common type is check-then-act, where a potentially stale observation is used
to decide what to do next.
Example: race condition in lazy initialization
@NotThreadSafepublic class LazyInitRace { private ExpensiveObject instance = null;
public ExpensiveObject getInstance() { if (instance == null) { instance = new ExpensiveObject(); } return instance; }}LazyInitRace has race conditions that can undermine its correctness. Say threads A and B execute
getInstance at the same time. A sees instance is null and instantiates a new
ExpensiveObject. B also checks if instance is null — whether it’s still null at that point
depends unpredictably on timing, including scheduling and how long A takes to instantiate the
object and set the field. If instance is still null when B examines it, the two callers may
receive two different results, even though getInstance is always supposed to return the same
instance.
The hit-counting operation in UnsafeCountingFactorizer has another kind of race condition.
Read-modify-write operations, like incrementing a counter, define a transformation of an
object’s state in terms of its previous state. To increment a counter safely, you need its
previous value, and you need to guarantee no one else changes or uses that value while you’re
mid-update.
Like most concurrency errors, race conditions don’t always cause failures — some unlucky timing
is also required. But they can cause serious problems. If LazyInitRace instantiated an
application-wide registry, returning different instances from multiple invocations could cause
registrations to be lost, or multiple activities to have inconsistent views of the registered set.
If UnsafeSequence generated entity IDs in a persistence framework, two distinct objects could
end up with the same ID, violating identity integrity constraints.
Compound actions
Both LazyInitRace and UnsafeCountingFactorizer contain a sequence of operations that need to
be atomic, or indivisible, relative to other operations on the same state. To avoid race
conditions, there must be a way to prevent other threads from using a variable while it’s being
modified, so other threads can only observe or modify the state before we start or after we
finish — never in the middle.
Operations A and B are atomic with respect to each other if, from the perspective of a thread executing A, when another thread executes B, either all of B has executed or none of it has. An operation is atomic if it’s atomic with respect to all operations (including itself) that operate on the same state.
If the increment in UnsafeSequence were atomic, the race condition above wouldn’t occur, and
each execution would have the desired effect of incrementing the counter by exactly one. To
ensure thread safety, check-then-act operations (like lazy initialization) and
read-modify-write operations (like increment) must always be atomic. Collectively, these are
called compound actions: sequences of operations that must execute atomically to remain
thread-safe.
Locking can ensure atomicity, but for now, we can fix the problem another way — by using an
existing thread-safe class, as shown in CountingFactorizer:
@ThreadSafepublic class CountingFactorizer implements Servlet { private final AtomicLong count = new AtomicLong(0);
public long getCount() { return count.get(); }
public void service(ServletRequest req, ServletResponse resp) { BigInteger i = extractFromRequest(req); BigInteger[] factors = factor(i); count.incrementAndGet(); encodeIntoResponse(resp, factors); }}The java.util.concurrent.atomic package contains atomic variable classes for effecting atomic
state transitions on numbers and object references. By replacing the long counter with an
AtomicLong, all actions that access the counter state become atomic. Since the servlet’s state
is the counter’s state, and the counter is now thread-safe, the servlet is thread-safe again.