Why Concurrency Matters
Modern computers have multiple CPU cores. Concurrency lets your Java programs take advantage of this parallelism — running multiple tasks simultaneously to improve performance and responsiveness. However, concurrent programming introduces complexity: race conditions, deadlocks, and visibility issues can cause hard-to-debug bugs.
Creating Threads in Java
Java provides two classic ways to create a thread:
1. Extending Thread
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("Running in: " + Thread.currentThread().getName());
}
}
// Usage
MyThread t = new MyThread();
t.start();
2. Implementing Runnable
Runnable task = () -> System.out.println("Task running!");
Thread t = new Thread(task);
t.start();
The Runnable approach (especially with lambdas) is generally preferred because it separates the task from the threading mechanism.
The Problem: Race Conditions
When multiple threads access shared mutable state without coordination, you get race conditions. Consider this broken counter:
int count = 0;
// Two threads both do: count++;
// Expected: 2. Actual: sometimes 1 (not atomic!)
The count++ operation is actually three steps: read, increment, write. Another thread can interleave between these steps.
Synchronization
The synchronized keyword ensures only one thread executes a block at a time:
public synchronized void increment() {
count++;
}
Alternatively, use java.util.concurrent.atomic classes for lock-free thread safety:
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // thread-safe
The Executor Framework
Raw threads are low-level. For real applications, use the ExecutorService from java.util.concurrent:
ExecutorService executor = Executors.newFixedThreadPool(4);
executor.submit(() -> {
System.out.println("Task 1 running");
});
executor.submit(() -> {
System.out.println("Task 2 running");
});
executor.shutdown();
Key executor types:
newFixedThreadPool(n)— A pool with a fixed number of threadsnewCachedThreadPool()— Creates threads as needed, reuses idle onesnewSingleThreadExecutor()— Exactly one thread, tasks run sequentiallynewScheduledThreadPool(n)— For scheduled/recurring tasks
Callable and Future
Unlike Runnable, Callable can return a result and throw checked exceptions. The result is wrapped in a Future:
Callable<Integer> task = () -> 42;
Future<Integer> future = executor.submit(task);
Integer result = future.get(); // blocks until done
System.out.println(result); // 42
CompletableFuture (Java 8+)
CompletableFuture enables non-blocking, composable async pipelines:
CompletableFuture.supplyAsync(() -> fetchData())
.thenApply(data -> process(data))
.thenAccept(result -> System.out.println(result))
.exceptionally(ex -> { ex.printStackTrace(); return null; });
Common Pitfalls to Avoid
- Deadlock — Two threads each waiting for a lock the other holds. Avoid by always acquiring locks in the same order.
- Visibility issues — Use
volatilefor simple flags shared between threads. - Over-synchronization — Locking too broadly kills performance. Lock only what needs protection.
- Not shutting down executors — Always call
shutdown()or your app may never terminate.
Mastering Java concurrency takes time, but the Executor framework and CompletableFuture give you powerful, high-level tools to build safe, efficient multithreaded applications.