HuntExams Academy logo
JavaIntermediate#concurrency

What is a race condition and how do you prevent it?

Answer

A race condition occurs when multiple threads access shared mutable state concurrently and the outcome depends on timing. Prevent it with synchronization, locks, atomic classes, or immutable/thread-confined data.

Example

// Without synchronization, count++ isn't atomic
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // thread-safe increment

Detailed explanation

A race condition occurs when a program's correctness depends on the timing or ordering of concurrent operations. In Java, it commonly appears when multiple threads access shared mutable state and at least one thread modifies it without suitable coordination. For example, two threads incrementing the same counter may lose an update. Such bugs matter because they can cause incorrect balances, duplicate processing, or inconsistent application state, yet remain hidden during ordinary testing.

The expression count++ is not atomic: it reads the current value, calculates the next value, and writes it back. If two threads both read 5 before either writes, both may write 6 instead of producing 7. Declaring count volatile does not fix this: volatile provides visibility and ordering guarantees, but does not make a read-modify-write sequence indivisible. More broadly, even individually thread-safe operations can form a race when combined, such as checking that a collection lacks a key and then inserting it.

Prevent races by protecting the complete operation that must behave as one unit. A synchronized block or method provides mutual exclusion when competing threads acquire the same monitor; releasing and subsequently acquiring that monitor also establishes visibility guarantees. ReentrantLock provides similar protection with features such as timed or interruptible acquisition, but unlock it in a finally block after successful acquisition. For a single counter, AtomicInteger count = new AtomicInteger(0); followed by count.incrementAndGet(); performs an atomic increment without an explicit application-level lock, typically using hardware-supported atomic instructions.

Choose the mechanism according to the invariant being protected. AtomicInteger does not automatically make a transfer involving two account balances atomic; that requires coordination across both updates. Concurrent collections offer atomic compound operations such as putIfAbsent, which are preferable to separate checks and writes. You can also avoid shared mutation through immutable objects, thread confinement, or message passing. Interviewers generally expect you to demonstrate a failing interleaving, distinguish atomicity from visibility, and explain why your chosen protection covers every relevant access.

Key points to remember

  • Race conditions make correctness depend on thread timing or operation ordering.
  • count++ is a read-modify-write sequence, not an atomic operation.
  • volatile provides visibility and ordering, not mutual exclusion or atomic increments.
  • Use the same monitor or lock consistently to protect the entire shared-state invariant.
  • Use atomic operations for suitable single-value updates, or avoid shared mutation through immutability and confinement.

Common mistakes candidates make

  • Assuming volatile makes count++ thread-safe.
  • Protecting only writes, using different locks, or locking individual steps instead of the whole compound operation.
  • Assuming atomic fields or thread-safe collections make every multi-step workflow atomic.

Likely follow-up questions

How is a race condition different from a data race?

A Java data race involves conflicting accesses to the same variable, at least one a write, that are not ordered by a happens-before relationship. A race condition is a broader correctness problem: a check-then-act workflow can still race even when its individual accesses are properly synchronized.

When should you choose AtomicInteger instead of synchronized?

Choose AtomicInteger when the required operation is an atomic update to a single integer, such as incrementing a counter. Use synchronization or another coordination mechanism when correctness depends on multiple fields or a multi-step invariant.

Does Thread.sleep() prevent race conditions?

No; sleeping changes scheduling but creates no mutual-exclusion or visibility guarantee. It may hide a bug temporarily, while synchronization or an appropriate atomic operation addresses the underlying problem.

Interview tip: Show how two increments can lose an update, then explain the smallest synchronization boundary that preserves correctness.

Related Interview Questions

1
JavaAdvanced#concurrency

What is a deadlock and how can it be avoided?

Open
2
JavaIntermediate#concurrency

What is the difference between synchronized methods and synchronized blocks?

Open
3
JavaIntermediate#concurrency

What are atomic classes like AtomicInteger used for?

Open