Java Programming
Threads, Synchronization, Thread Groups and Concurrency
PGCP-BDA
process and thread
A process owns an execution environment and resources; threads are concurrent execution paths sharing their process memory.
Thread class
Thread represents a Java thread and supplies lifecycle, naming, interruption, priority and coordination operations.
Runnable
Runnable separates a no-result task from the Thread that executes it and works with executors as well as direct threads.
thread lifecycle
A Java thread moves among NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING and TERMINATED according to scheduling and coordination.
start and run
start asks the JVM to create concurrent execution that invokes run; calling run directly is an ordinary method call on the current thread.
join and interrupt
join waits for another thread to terminate; interrupt requests cooperative cancellation or wakes many blocking operations with defined status behavior.
synchronized
A synchronized method or block acquires an object monitor, providing mutual exclusion and happens-before visibility at lock release and acquisition.
visibility
Memory visibility determines when one thread’s writes are guaranteed observable by another through locks, volatile variables.
race condition
A race condition makes correctness depend on uncontrolled relative timing of concurrent operations on shared state.
deadlock
Deadlock occurs when threads wait permanently in a cycle for resources held by one another.
Deadlock occurs when threads wait indefinitely in a cycle:
Thread A holds Lock 1 and waits for Lock 2
Thread B holds Lock 2 and waits for Lock 1
Typical necessary conditions include mutual exclusion, holding one resource while waiting for another, inability to forcibly take resources and a circular wait.
Prevent lock-order deadlock by assigning a global order and acquiring multiple locks in that order. Other strategies include avoiding nested locks, using tryLock with timeout and retry, reducing shared state or combining related state under one lock. Thread dumps help diagnose which threads own and await locks.
concurrent collection
Concurrent collections define thread-safe operations and iteration semantics designed for shared access without one external global lock.
Thread lifecycle and states
Thread.State contains:
| State | Meaning |
|---|---|
NEW | Constructed but not started |
RUNNABLE | Eligible to run or running |
BLOCKED | Waiting to acquire an intrinsic monitor |
WAITING | Waiting indefinitely for another action |
TIMED_WAITING | Waiting for a bounded time |
TERMINATED | Execution completed |
Java does not expose a separate portable “running” state; running threads are RUNNABLE. A thread waiting for a synchronized monitor is BLOCKED, while a thread in Object.wait() or an untimed join() is WAITING.
State observations are snapshots and can change immediately. They are useful for diagnosis, not for coordinating application logic.
Processes, threads and tasks
A process has its own runtime resources and memory space. Threads within one Java process share heap objects while each thread has its own call stack and program counter. Sharing makes communication efficient, but mutable shared objects require coordination.
A task describes work; a thread is one mechanism that executes it:
Runnable task = () ->
System.out.println(Thread.currentThread().getName());
Thread worker = new Thread(task, "report-worker");
worker.start();
start() asks the JVM to create a new execution path that later invokes run(). Calling worker.run() directly is an ordinary method call on the current thread. A Thread object can be started only once; a second start() throws IllegalThreadStateException.
Sleep, join and yield
Thread.sleep(duration) pauses the current thread for at least approximately the requested time, subject to scheduling and interruption. Sleep does not release intrinsic monitors the thread already owns.
thread.join() waits for that thread to terminate:
Thread worker = new Thread(task);
worker.start();
worker.join();
System.out.println("Worker has finished");
yield() is a scheduling hint that the current thread is willing to let another runnable thread proceed. The scheduler may ignore it. Priorities are also hints and do not guarantee execution order, fairness or timely completion.
Never coordinate correctness using sleeps, yields, priorities or assumed scheduling order.
Priorities and thread groups
Java thread priorities range from Thread.MIN_PRIORITY through Thread.MAX_PRIORITY, with NORM_PRIORITY as the conventional default. setPriority supplies a scheduling hint whose practical effect depends on the JVM and operating system. It cannot establish ordering, mutual exclusion or a deadline.
ThreadGroup is an older facility that places threads into a hierarchy for bulk inspection and uncaught-exception handling. A group can enumerate or interrupt member threads, but enumeration is only a changing snapshot and broad interruption may affect unrelated work. Modern applications normally organize tasks with executors, named thread factories, structured ownership and explicit cancellation rather than using ThreadGroup as the main concurrency abstraction.
Race conditions
A race condition occurs when correctness depends on uncontrolled timing. The expression count++ is a read–modify–write sequence:
- read count;
- add one;
- write the result.
If two threads both read 5 before either stores, both may write 6 and one increment is lost. The final result varies with interleaving.
A check-then-act sequence is also a compound race:
if (!map.containsKey(key)) {
map.put(key, createValue());
}
Another thread can act between the check and update. Concurrent designs need one atomic operation, such as computeIfAbsent where its contract fits or a lock covering the entire invariant.
Java memory visibility
Without a happens-before relationship, one thread is not guaranteed to observe another thread's recent writes. Compilers, processors and caches may reorder or retain values within the Java Memory Model's permissions.
Happens-before relationships arise from mechanisms including:
- releasing and subsequently acquiring the same monitor;
- writing and subsequently reading the same volatile variable;
- actions before
Thread.start()becoming visible to the started thread; - actions in a thread becoming visible after another thread successfully returns from
join(); - synchronization rules provided by concurrent utilities.
Happens-before supplies visibility and ordering. It does not make an arbitrary multi-step algorithm atomic.
Concurrent collections
ConcurrentHashMap supports scalable concurrent access and atomic operations such as putIfAbsent, compute and merge. It rejects null keys and values so null can indicate absence unambiguously.
CopyOnWriteArrayList creates a new backing array for each write. Iteration is stable and lock-free over a snapshot, making it useful for small, read-mostly listener lists but expensive for frequent updates.
Blocking queues coordinate producers and consumers. Concurrent queues and deques support non-blocking access patterns. A synchronized wrapper protects individual collection methods, but a sequence of calls still requires external synchronization on the documented wrapper monitor.
Intrinsic synchronization
Every Java object has an intrinsic monitor. A synchronized block acquires it:
private final Object lock = new Object();
private int balance;
void deposit(int amount) {
synchronized (lock) {
balance += amount;
}
}
Only one thread at a time executes code guarded by the same monitor. Exiting releases the monitor even if an exception occurs. Release and later acquisition also establish visibility.
An instance synchronized method locks this. A static synchronized method locks the Class object, such as Account.class. These are different monitors, so instance and static synchronization do not automatically exclude each other.
Synchronize all access participating in one invariant under the same policy. Locking writes while leaving related reads unlocked can still expose stale or inconsistent state.
Runnable, Callable and separation of concerns
Runnable.run() returns no value and cannot declare checked exceptions. Callable<V>.call() returns a value and may throw:
Callable<Integer> countLines = () -> {
try (Stream<String> lines = Files.lines(path)) {
return Math.toIntExact(lines.count());
}
};
Keeping work in Runnable or Callable separates task definition from thread creation, scheduling, pooling and shutdown. Extending Thread couples those responsibilities and also consumes Java's one class-inheritance choice.
Uncaught exceptions terminate the affected thread unless an uncaught-exception handler processes them. An executor captures task failures in a Future rather than necessarily sending them to a thread handler.
Executors and thread pools
An ExecutorService manages worker threads and submitted tasks:
ExecutorService pool = Executors.newFixedThreadPool(4);
try {
pool.submit(task);
} finally {
pool.shutdown();
}
A fixed pool limits concurrent workers and queues excess work. A cached pool can create many threads and needs careful workload control. A single-thread executor serializes tasks. ScheduledExecutorService performs delayed or periodic work and is preferable to manually sleeping a loop.
Production systems often construct ThreadPoolExecutor explicitly to control queue capacity, thread creation, names, rejection policy and metrics. An unbounded queue can hide overload until memory or latency becomes unacceptable.
Wait, notify and guarded blocks
wait, notify and notifyAll are Object methods and operate on that object's monitor. The caller must own the monitor or IllegalMonitorStateException occurs.
synchronized (queue) {
while (queue.isEmpty()) {
queue.wait();
}
item = queue.remove();
}
wait() releases the relevant monitor and enters waiting. Before returning, the thread reacquires the same monitor. Check the condition in a while loop because wakeups may be spurious, another thread may consume the condition first or notification may concern a different condition.
notify() selects one waiter, while notifyAll() wakes all waiters to compete. Neither transfers the lock immediately; the notifying thread keeps it until leaving the synchronized region.
Liveness problems beyond deadlock
Starvation occurs when a thread repeatedly fails to obtain CPU time or a contended resource. Livelock occurs when threads keep responding to each other and changing state but make no useful progress. Contention reduces throughput as threads compete for synchronization.
Fair locks can reduce starvation in some cases but add overhead and still do not provide general application-level fairness. Randomized backoff can help livelock. The best solution often reduces shared mutable state or partitions ownership.
Interruption and cancellation
Interruption is a cooperative signal. Blocking methods such as sleep, wait, join and many queue operations respond by throwing InterruptedException, generally clearing the interrupted status.
A method that cannot propagate the exception should often restore the status:
try {
queue.put(item);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return;
}
Do not silently swallow interruption and continue as if the operation succeeded. Define whether interruption means cancellation, a request to finish current work or an application-specific wakeup. Long CPU loops should periodically check isInterrupted().
Explicit locks
ReentrantLock offers capabilities beyond synchronized, including interruptible acquisition, timed attempts, optional fairness and multiple Condition objects:
lock.lock();
try {
updateState();
} finally {
lock.unlock();
}
Unlocking in finally is mandatory. Unlike synchronized, an explicit lock is not automatically released by leaving a block.
ReadWriteLock permits several readers or one writer, but it helps only when reads are frequent, sections are meaningful and contention justifies added complexity. Measure rather than assuming it is faster.
Worked interleaving
Assume count is 5 and two threads execute count++ without synchronization:
Thread A reads 5
Thread B reads 5
Thread A computes 6
Thread B computes 6
Thread A stores 6
Thread B stores 6
Two increments produced 6 rather than 7. Declaring count volatile would make reads and writes visible but would not combine them into one operation. AtomicInteger.incrementAndGet() or a synchronized critical section protects the complete transition.
Safe publication and immutability
An object should not be made visible to other threads before construction is complete. Safe publication can use a static initializer, volatile reference, lock, thread-safe collection or other happens-before mechanism.
Immutable objects are easier to share because their state never changes after construction. Correctly constructed final fields have special visibility guarantees, but allowing this to escape from a constructor can undermine safe initialization.
Confinement avoids sharing altogether. Local variables are naturally thread-confined and actor or message-passing designs assign mutable state to one owner.
Producer–consumer with BlockingQueue
BlockingQueue encodes common producer–consumer coordination:
BlockingQueue<Job> jobs = new ArrayBlockingQueue<>(100);
// producer
jobs.put(job); // waits while full
// consumer
Job next = jobs.take(); // waits while empty
Bounded queues create backpressure so producers cannot consume unlimited memory when consumers are slower. Methods such as offer with a timeout allow explicit failure policies.
Using BlockingQueue is safer than manually combining a collection, monitor, size condition and notifications. The higher-level utility centralizes a well-tested synchronization contract.
Lock scope and reentrancy
Keep critical sections small enough to reduce contention, but large enough to protect the complete operation. Avoid slow network calls or callbacks while holding a lock unless the design explicitly requires it.
Intrinsic locks are reentrant: a thread holding a monitor can acquire it again. This allows one synchronized method to call another synchronized method on the same receiver without deadlocking itself.
Do not synchronize on publicly accessible or replaceable objects such as string literals, boxed values or a mutable lock field. A private final lock object prevents outside code from unexpectedly sharing the monitor.
Atomic classes
java.util.concurrent.atomic supplies lock-free atomic operations for common single-variable cases:
AtomicInteger count = new AtomicInteger();
int updated = count.incrementAndGet();
Compare-and-set changes a value only when it still equals an expected value:
boolean changed = state.compareAndSet(expected, replacement);
AtomicReference can atomically replace immutable state. LongAdder can scale better than AtomicLong under high write contention when an immediately consistent total is not required at every moment.
Atomics protect their operation, not a broader invariant spanning several independent variables. A lock may be clearer for complex state transitions.
ThreadLocal
ThreadLocal<T> provides a separate value associated with each thread:
private static final ThreadLocal<DateTimeFormatter> FORMATTER =
ThreadLocal.withInitial(
() -> DateTimeFormatter.ofPattern("dd-MM-uuuu"));
It does not create a shared value and is not a general replacement for method parameters. Thread pools reuse worker threads, so stale ThreadLocal values can leak into later tasks or retain objects. Remove values in finally when their lifetime belongs to one task:
try {
context.set(requestContext);
handle();
} finally {
context.remove();
}
Prefer immutable thread-safe objects directly when possible; DateTimeFormatter, for example, does not require ThreadLocal.
Volatile variables
A volatile write becomes visible to a subsequent volatile read of the same variable, with associated ordering guarantees:
private volatile boolean shutdown;
void requestShutdown() { shutdown = true; }
void work() {
while (!shutdown) {
performUnit();
}
}
Volatile is suitable for independently updated state such as flags or safely published immutable snapshots. It does not make compound operations atomic:
volatile int count;
count++; // still read, modify, write
When multiple fields must change together or a new value depends on the old one, use locking, an atomic compound operation or an immutable-state replacement design.
Futures and task failure
Submitting a Callable returns Future<V>:
Future<Integer> future = pool.submit(countLines);
try {
int count = future.get();
} catch (ExecutionException ex) {
Throwable taskFailure = ex.getCause();
}
get() waits for completion and wraps task failure in ExecutionException. A timed get prevents indefinite waiting. cancel(true) requests interruption if the task is running; it cannot force code that ignores interruption to stop.
CompletableFuture supports dependent asynchronous stages. Distinguish thenApply for a direct transformed value from thenCompose when the function already returns another CompletionStage.
Continue learning
Related notes
Put this topic into timed practice
Open mock tests when you want full-exam pacing, or keep drilling in practice mode.