Exception Handling & Multithreading
try/catch/finally, custom exceptions, threads, the Executor framework, and thread-safety basics.
Why exceptions exist
Instead of returning error codes that are easy to ignore, Java forces problems to surface as exceptions — objects that describe what went wrong, thrown up the call stack until something handles them.
public class Main {
public static void main(String[] args) {
try {
int result = 10 / 0;
} catch (ArithmeticException e) {
System.out.println("Can't divide by zero: " + e.getMessage());
} finally {
System.out.println("This always runs — cleanup goes here");
}
}
}
Can't divide by zero: / by zero
This always runs — cleanup goes here
Checked vs unchecked exceptions
| Type | Examples | Must be declared/caught? |
|---|---|---|
| Checked | IOException, SQLException |
Yes — the compiler forces you to handle or declare (throws) them |
Unchecked (RuntimeException) |
NullPointerException, IllegalArgumentException |
No — usually programming bugs, not recoverable conditions |
| Error | OutOfMemoryError, StackOverflowError |
No — serious JVM-level problems, not meant to be caught |
Custom exceptions
public class InsufficientFundsException extends Exception {
public InsufficientFundsException(String message) {
super(message);
}
}
public class BankAccount {
private double balance;
public void withdraw(double amount) throws InsufficientFundsException {
if (amount > balance) {
throw new InsufficientFundsException("Not enough balance");
}
balance -= amount;
}
}
try-with-resources
Resources (files, database connections) that implement AutoCloseable get closed automatically, even if an exception is thrown — no finally block needed:
try (var reader = new BufferedReader(new FileReader("data.txt"))) {
System.out.println(reader.readLine());
} catch (IOException e) {
System.out.println("Failed to read file: " + e.getMessage());
}
Threads and concurrency
A thread is an independent path of execution within a program. The JVM can run multiple threads in parallel on multi-core CPUs.
Runnable task = () -> {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
};
Thread t1 = new Thread(task, "Worker-1");
Thread t2 = new Thread(task, "Worker-2");
t1.start();
t2.start();
Because both threads run concurrently, their output can interleave unpredictably — this is exactly why shared state needs protection.
The Executor framework (preferred over raw threads)
Managing raw Thread objects doesn't scale. The ExecutorService manages a pool of reusable worker threads for you:
import java.util.concurrent.*;
ExecutorService pool = Executors.newFixedThreadPool(4);
Future<Integer> future = pool.submit(() -> {
Thread.sleep(100);
return 42;
});
System.out.println("Result: " + future.get()); // blocks until the task finishes
pool.shutdown();
Thread safety
When multiple threads mutate shared state without coordination, you get race conditions. synchronized ensures only one thread executes a block at a time:
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int get() {
return count;
}
}
For simple counters, java.util.concurrent.atomic.AtomicInteger is faster and lock-free:
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();
Common mistakes
- Catching
Exception(or worse,Throwable) too broadly, silently swallowing real bugs. - Forgetting
finally/try-with-resources, leaking file handles or database connections. - Mutating shared state from multiple threads without
synchronized, anAtomic*type, or a concurrent collection — a classic source of flaky, hard-to-reproduce bugs. - Creating a new raw
Threadper task in a hot path instead of reusing anExecutorServicepool.
Interview questions
Q: What's the difference between checked and unchecked exceptions?
Checked exceptions (subclasses of Exception other than RuntimeException) must be declared with throws or caught — the compiler enforces handling. Unchecked exceptions (RuntimeException and its subclasses) are not enforced by the compiler and usually indicate programming bugs.
Q: Why prefer ExecutorService over creating raw Thread objects?
Thread creation is expensive, and unbounded thread creation can exhaust system resources. An ExecutorService reuses a bounded pool of threads, queues excess work, and gives you Future-based results and clean shutdown.
Q: What causes a race condition? Two or more threads reading and writing shared mutable state without synchronization, so the final result depends on unpredictable thread scheduling instead of program logic.