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 threads
  • newCachedThreadPool() — Creates threads as needed, reuses idle ones
  • newSingleThreadExecutor() — Exactly one thread, tasks run sequentially
  • newScheduledThreadPool(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 volatile for 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.