Advanced Java Interview Mastery
Multithreading
Running Tasks in Parallel
Most programs you've written so far probably run step-by-step, from top to bottom. This is called single-threaded execution. One task must finish completely before the next one can begin. But modern computers have multiple processor cores, meaning they can do several things at once. Multithreading is how we take advantage of that power.
Think of it like a kitchen. A single-threaded kitchen has one chef who does everything in order: chop vegetables, then cook the meat, then prepare the sauce. Nothing happens simultaneously. A multithreaded kitchen has multiple chefs. One chops vegetables while another cooks the meat, and a third prepares the sauce. The whole meal gets done much faster.
In Java, each of these "chefs" is a thread. A thread is the smallest unit of execution within a program. By using multiple threads, an application can perform several operations concurrently, leading to better performance and a more responsive user experience. For example, one thread can handle user input in an application's interface while another performs a complex calculation in the background.
Multithreading is a technique that allows for concurrent (simultaneous) execution of two or more parts of a program for maximum utilization of a CPU.
This allows your application to remain responsive. If a long-running task is executed on the main application thread, the user interface might freeze until it's done. By moving that task to a separate thread, the UI remains fluid and interactive.
The Life of a Thread
A thread isn't just running or not running. It moves through several distinct states during its existence, from birth to death. Understanding this lifecycle is key to managing threads effectively.
Here's a breakdown of the states:
- NEW: The thread has been created but hasn't started yet. It's an object, but it's not alive.
- RUNNABLE: Once you call the
start()method, the thread enters the runnable state. It's now eligible to be run by the thread scheduler, but it might be waiting for its turn on the CPU. - BLOCKED/WAITING: The thread is alive but currently not eligible to run. It's waiting for something to happen, like another thread to release a lock it needs, or for another thread to explicitly wake it up.
- TIMED_WAITING: This is similar to the waiting state, but the thread will only wait for a specific period. It can be woken up earlier, but it will wake up automatically after the timeout expires.
- TERMINATED: The thread has finished its job. Its
run()method has completed, and it cannot be restarted.
Putting Threads to Work
In Java, you can create threads in two main ways. The first is to extend the Thread class and override its run() method. The run() method contains the code that the thread will execute.
class MyThread extends Thread {
public void run() {
System.out.println("MyThread is running.");
}
}
// To use it:
MyThread t1 = new MyThread();
t1.start(); // This starts the new thread.
The second, and more common, approach is to implement the Runnable interface. This is often preferred because it separates your task (the Runnable) from the execution mechanism (the Thread). It also allows your task class to extend a different class if needed, since Java doesn't support multiple inheritance.
class MyRunnable implements Runnable {
public void run() {
System.out.println("MyRunnable is running.");
}
}
// To use it:
MyRunnable myTask = new MyRunnable();
Thread t2 = new Thread(myTask);
t2.start();
Crucially, you always call the
start()method to begin a thread's execution. Callingrun()directly would simply execute the code in the current thread, not a new one.
The Sharing Problem
Multithreading introduces a powerful capability, but it also brings a significant challenge: managing access to shared resources. What happens when two threads try to modify the same variable at the exact same time? The result can be unpredictable and lead to corrupted data. This is known as a race condition.
Imagine two threads trying to increment a shared counter that starts at 0. Both threads read the value (0), add 1 to it, and write the result back. If they do this at the same time, they both write 1 back to the counter. The final value is 1, not 2 as it should be. This kind of bug is notoriously difficult to track down because it depends on the precise timing of thread execution.
Synchronization
noun
The coordination of two or more threads to ensure they do not interfere with each other when accessing shared data or resources.
To solve this, we need synchronization. Java provides the synchronized keyword, which acts as a lock. A synchronized method or block can only be executed by one thread at a time. If a thread tries to enter a synchronized block that is already in use by another thread, it will be blocked until the lock is released.
public class SafeCounter {
private int count = 0;
// Only one thread can execute this method at a time
// on a given instance of SafeCounter.
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
By making the increment() method synchronized, we ensure that the read-modify-write operation is atomic—it happens as a single, indivisible unit. This prevents race conditions and guarantees that our counter works correctly even with multiple threads.
Synchronization is powerful, but it's not free. Acquiring and releasing locks has a performance cost. Overusing it can slow down your application and even lead to other problems like deadlock, where two or more threads are stuck waiting for each other to release locks. The key is to protect only the critical sections of your code that access shared, mutable state.
In the context of multithreading, if a single-threaded program is like one chef doing all kitchen tasks sequentially, what is a multithreaded program analogous to?
Immediately after the start() method is called on a newly created Java Thread object, what state does the thread enter?
Multithreading is a deep topic, but understanding these core concepts—the lifecycle, thread creation, and synchronization—provides a solid foundation for building powerful, concurrent applications in Java.