Concurrency - Threads and Synchronization
Understand Java thread lifecycle, synchronization, locks, and race conditions with practical examples.
#java #concurrency #threads #synchronization
Why this step matters
Modern backend services handle many requests simultaneously. Without concurrency basics, shared state can become inconsistent and create non-deterministic bugs.
Thread lifecycle basics
A thread typically moves through:
- new
- runnable/running
- blocked or waiting
- terminated
Thread t = new Thread(() -> System.out.println("worker"));
t.start();
Race condition example
A race condition happens when multiple threads update shared state without coordination.
class Counter {
int value = 0;
void increment() {
value++; // not atomic
}
}
Two threads can read the same value and overwrite each other.
synchronized
Use synchronized to guard critical sections.
class SafeCounter {
private int value = 0;
synchronized void increment() {
value++;
}
synchronized int get() {
return value;
}
}
This ensures one thread enters the synchronized method at a time.
Locks (ReentrantLock)
Locks give more control than synchronized.
Lock lock = new ReentrantLock();
lock.lock();
try {
// critical section
} finally {
lock.unlock();
}
Use locks when you need features like tryLock() or fair locking.
Visibility and atomicity
volatile: visibility guarantee for reads/writes, not full atomic updatesAtomicInteger: atomic operations without explicit lock for simple counters
AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet();
Common mistakes
- sharing mutable objects without synchronization
- locking too much code (performance bottleneck)
- forgetting
unlock()in lock-based code - assuming bugs will be easy to reproduce
Takeaway
- Understand race conditions and shared mutable state risk
- Use
synchronizedor locks for critical sections - Prefer atomic classes for simple shared counters
- Keep thread-safe design explicit and minimal