Thread Lifecycle
In Java, multithreading is defined by the fundamental concept of Thread. During their lifecycle, threads go through various states:
NEW - a newly created thread that has not yet started execution
RUNNABLE - either running or ready to execute, but waiting for resource allocation
BLOCKED - waiting to acquire a monitor lock to enter or re-enter a synchronized block/method
WAITING - waiting for another thread to perform a specific action without time limit
TIMED_WAITING - waiting for another thread to perform a specific action for a specified period of time
TERMINATED - has completed execution.
Open: Pasted image 20251121132137.png

Runnable vs Extending a Thread
Simply put, we prefer using Runnable over Thread:
By extending the Thread class, we don’t override any of its methods. Instead, we override the Runnable method (which, as it turns out, implements Thread). This is a clear violation of the IS-A Thread principle
Creating a Runnable implementation and passing it to the Thread class uses composition instead of inheritance, which is more flexible.
Starting from Java 8, Runnables can be represented as lambda expressions.
Runnable vs. Callable
Both interfaces are designed to represent a task that can be executed by multiple threads. Runnable tasks can be run using the Thread class or ExecutorService, while Callables can only be run with the latter.
wait(), notify(), notifyAll()
wait()— the thread waits and releases the monitor.notify()— wakes up one thread waiting on this object.notifyAll()— wakes up all.
Synchronized
We can use the synchronized keyword at different levels:
- Instance methods
- Static methods
- Code blocks
When we use a synchronized block, Java internally uses a monitor, also known as a monitor lock or intrinsic lock, to provide synchronization. These monitors are bound to objects, so all synchronized blocks of the same object can only be executed by one thread at a time.
Static methods are synchronized the same way as instance methods.
These methods are synchronized on the Class object associated with the class. Since there is only one Class object per class in the JVM, only one thread can execute inside a static synchronized method for a class, regardless of the number of its instances.
Sometimes we don’t want to synchronize the entire method, but only some instructions within it. This can be achieved by applying synchronized to a block:
public void exampleSyncBlock() {
synchronized(this) {
setCount(getCount() + 1)
}
}Note that we passed the this parameter to the synchronized block. This is the monitor object. The code inside the block is synchronized on the monitor object. Simply put, only one thread per monitor object can execute inside this code block.
If the method were static, instead of the object reference, we would pass the class name, and the class would become the monitor for synchronizing the block.
Volatile
We can use volatile to solve cache consistency issues.
For variable updates to be predictably propagated to other threads, the volatile modifier must be applied to them.
This way, we can communicate with the runtime and processor not to reorder instructions related to the volatile variable. Additionally, processors understand that they must immediately flush any updates of these variables.
volatile - is a very useful keyword because it can be used to ensure visibility of data changes without providing mutual exclusion. Therefore, it is useful in cases where we don’t mind a code block being executed by multiple threads concurrently, but we need to ensure the visibility property.
Technically, any write to a volatile field happens before any subsequent read of the same field. This is the volatile variable rule of the Java Memory Model (JMM).
Happens-before
Happens-before — is a fundamental rule of the Java Memory Model that defines which changes from one thread are guaranteed to be visible to another thread.
In short:
If action A happens-before action B, then all effects of A (memory writes) are guaranteed to be visible to the thread performing B.
No race conditions, no “floating values” — visibility and order of actions are guaranteed.
Memory Visibility
In a multithreaded environment, each thread can work with its own copies of data lying in the CPU core cache.
When one thread updates variables, there is no guarantee that another thread will immediately see these changes in main memory.
Due to processor and compiler optimizations, the reading thread may:
- see the old value,
- see the new one, but with delay,
- see values in a different order than they were written in the code. This is why ordinary variables without synchronization (synchronized, volatile, locks) do not provide visibility guarantees.
Locks
Types of Locks
- synchronized block: This is a keyword in Java that can be used to restrict code access to only one thread. This block can be used to synchronize methods or code blocks.
- ReentrantLock: This is a class implementing the Lock interface that provides more flexible locking capabilities. It allows for more complex locking scenarios, such as locking with timed waiting, locking with interrupt attempt, and others.
- ReadWriteLock: This is an interface providing two locks - one for reading and one for writing. This scheme allows multiple threads to perform read operations simultaneously, but blocks write access during data updates.
- StampedLock: lock with write/read support and ultra-fast optimistic read, which speeds up reading and reduces contention in multithreading.
Deadlock vs Livelock
Deadlock (Mutual Blocking)
Threads block each other forever, each waiting for a resource held by the other.
Result: The system stands still, there is no progress.
Example:
Thread A holds resource 1 and waits for resource 2.
Thread B holds resource 2 and waits for resource 1.
How to avoid:
- Always lock resources in the same order.
- Use timeouts when acquiring locks (
tryLock(timeout)). - Minimize the number of simultaneously acquired locks.
Livelock (Living Lock)
Threads are not blocked, but move uselessly, constantly trying to avoid conflict and interfering with each other.
Result: The system is working, but there is also no progress.
Example:
Two threads yield a resource to each other, refuse and try again, but synchronously and endlessly.
How to avoid:
- Add random delays or exponential backoff on retry attempts.
- Use explicit timeouts and stop attempts after a certain time.
- Reconsider the cooperation algorithm to not block each other continuously.
Atomics
Atomics in Java work as operations that guarantee atomicity and visibility of changes in shared variables for multithreaded programs. Atomics in Java are implemented through classes from the java.util.concurrent.atomic package
Atomic operations guarantee that the operation will be completed fully and no other thread can change the variable’s value until the operation completes.
Atomics do not use locks. They ensure atomicity through CAS — a special atomic processor instruction that allows updating a value only if no one has changed it between reading and writing.
CountDownLatch
CountDownLatch (appeared in JDK 5) - is a utility class that blocks a set of threads until some operation completes.
CountDownLatch is initialized with a counter (Integer type) that decreases as dependent threads complete execution. But once the counter reaches zero, other threads are released.
CyclicBarrier
CyclicBarrier works almost the same as CountDownLatch, except that we can reuse it. Unlike CountDownLatch, it allows multiple threads to wait for each other using the await() method (known as the barrier condition) before calling the final task.
Semaphore
A semaphore is used to block thread-level access to some part of a physical or logical resource. A semaphore contains a set of permits; whenever a thread tries to enter a critical section, it must check the semaphore for the presence or absence of a permit.
Phaser
Phaser is a more flexible solution than CyclicBarrier and CountDownLatch - it is used as a reusable barrier where a dynamic number of threads must wait before continuing execution. We can coordinate multiple phases of execution by reusing the Phaser instance for each phase of the program.
Concurrent Collections
These are collections from java.util.concurrent, safe for multithreaded access without external synchronization.
Examples:
ConcurrentHashMap— thread-safe map, supports fast read and write operations.CopyOnWriteArrayList— thread-safe list that copies the internal array on modification.
ConcurrentHashMap is often used for consistent cache: multiple threads can read and update data simultaneously, while the map remains in a correct state. This is especially convenient for storing intermediate or computed values that are frequently accessed by multiple threads.
Executor
This is an interface representing an object that executes submitted tasks.
The specific implementation determines whether the task will be executed in a new or current thread. Thus, using this interface, we can separate the thread execution of the task from the actual execution mechanism.
It should be noted that Executor does not require task execution to be asynchronous. In the simplest case, an executor can invoke the submitted task instantly in the calling thread. If the Executor executor cannot accept a task for execution, it will throw a RejectedExecutionException message
ExecutorService
ExecutorService represents a comprehensive solution for asynchronous processing. It manages an in-memory queue and schedules job execution depending on thread availability:
ExecutorService excecutor = Executors.newFixedThreadPool(10);excecutor.submit(() -> {
//executor task
})Future
Future represents the future result of an asynchronous computation. This result will eventually appear in the Future after processing completes.
Moreover, the cancel(boolean mayInterruptIfRunning) API cancels the operation and releases the executing thread. If the value of the mayInterruptIfRunning parameter is true, the thread executing the task will be immediately terminated.
CompletableFuture
This is an extension of the Future class that provides the ability to work more flexibly with asynchronous operations. CompletableFuture allows explicit control over operation completion, combining multiple operations into a chain, and performing additional actions upon its completion.
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { // Asynchronous operation execution return "Operation result";});
Key points:
thenApply — applies a function to the result of the previous task, returns a new CompletableFuture.
cf.thenApply(result -> result * 2);
thenCompose — unwraps a nested CompletableFuture, used for sequential asynchronous execution.
cf.thenCompose(result -> asyncTask(result));
allOf / anyOf — combine multiple CompletableFuture:
- allOf — waits for all tasks to complete.
- anyOf — returns the result of the first completed task.
CompletableFuture.allOf(cf1, cf2).join();
CompletableFuture.anyOf(cf1, cf2).thenAccept(System.out::println);Asynchronous execution — tasks can be launched in a separate thread pool:
CompletableFuture.supplyAsync(() -> compute(), executor);ForkJoinPool
Central part of the fork/join framework (Java 7). Allows efficient execution of recursive tasks without creating a separate thread for each subtask. Tasks can be forked (split into subtasks) and joined (wait for their completion). Through the work-stealing algorithm, threads redistribute work among themselves, which increases efficiency.
Recommendations
- Before the interview, ask ChatGPT or search the internet for code review tasks on multithreading. Often in interviews, tasks are specifically about reviewing current code, finding errors, and making corrections.
- Practice writing small examples with
ExecutorService,CompletableFuture,CountDownLatch, andSemaphore. In interviews, you’re often asked to explain how code works, and practice with real examples helps you navigate quickly.
Conclusion
Today we covered key multithreading topics that most often come up in Java interviews. Of course, not everything from this list will necessarily be asked, but with it, you definitely won’t be “out of the loop”. I compiled it based on my own experience going through interviews — all of this was really asked and continues to be asked.