⚡ Modern Java Concurrency: Threads, Executors & Virtual Threads

Understanding Java's multi-threading and concurrency mechanisms is essential for writing performant, safe, and maintainable applications. This post explores thread creation, synchronization, executors, modern virtual threads, common concurrency problems, and tools for diagnosing issues.

1. ๐Ÿงต Thread Creation

  • Extending Thread: Override run().
  • class MyThread extends Thread {
        public void run() {
            System.out.println("Thread running");
        }
    }
    new MyThread().start();
  • Implementing Runnable: Pass to Thread.
  • Runnable task = () -> System.out.println("Runnable running");
    new Thread(task).start();
  • Callable & Future: Return value from thread.
  • Callable<Integer> task = () -> 42;
    Future<Integer> future = Executors.newSingleThreadExecutor().submit(task);
    System.out.println(future.get());
  • Virtual Threads (Java 19+): Lightweight threads.
  • try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
        executor.submit(() -> System.out.println("Virtual thread running"));
    }

2. ๐Ÿ”’ Synchronization & Locks

Ensure only one thread accesses critical sections:

  • synchronized: Built-in object/method lock.
  • ReentrantLock / ReadWriteLock / StampedLock: Advanced locking.
  • Semaphores: Limit concurrent access.
  • CountDownLatch / Phaser / CyclicBarrier: Coordinate multiple threads.

3. ๐Ÿ— Executor Framework

  • Interfaces: Executor, ExecutorService, ThreadPoolExecutor
  • Factories:
    • newSingleThreadExecutor()
    • newFixedThreadPool(n)
    • newCachedThreadPool()
    • newScheduledThreadPool(n)
    • newVirtualThreadPerTaskExecutor() (Java 19+)
  • Methods:
    • execute(Runnable) - no result
    • submit(Runnable / Callable) - returns Future
    • invokeAll(Collection<Callable>) / invokeAny(...) - execute multiple tasks

4. ⚠️ Common Concurrency Problems

  • Deadlock: Two threads wait forever for each other's locks.
  • Thread 1: lock(A) then lock(B)
    Thread 2: lock(B) then lock(A) → deadlock
  • Race Condition: Multiple threads modify shared data concurrently.
  • int counter = 0;
    Runnable r = () -> counter++;
    // multiple threads increment → unexpected results
  • Starvation: Low-priority threads never run.
  • Livelock: Threads actively retry but never complete.

These are some of the most common pitfalls developers face when working with concurrency. But what if, instead of constantly managing locks and synchronization, we could design our system so these problems don’t happen in the first place?

5 ๐Ÿงฉ Key Concurrency Concepts

1. new Thread() vs ExecutorService

new Thread() creates and starts a single thread manually. It’s simple but not scalable, because you have to manage the thread’s lifecycle, error handling, and resource limits yourself.

ExecutorService, on the other hand, is a high-level API that manages a pool of reusable threads. You submit Runnable or Callable tasks, and the framework handles scheduling, queuing, and thread reuse efficiently. It also provides better control — you can gracefully shut it down, limit concurrency, and integrate with features like Future or virtual threads.

2. synchronized vs ReentrantLock

Both synchronized and ReentrantLock ensure that only one thread can access a critical section at a time.

The key difference is control and flexibility:

  • synchronized is simpler — locking and unlocking happen automatically when entering or exiting a synchronized block or method.
  • ReentrantLock provides explicit control, allowing you to use features like tryLock() (with timeout), interruptible locking, or fairness ordering (FIFO). It also supports Condition objects for advanced wait/notify behavior.

In modern Java (since 1.6+), the performance gap is smaller due to JVM optimizations, so synchronized is preferred unless advanced control is needed.

3. volatile vs AtomicInteger

volatile ensures visibility — every read reflects the latest value written by any thread, avoiding caching issues. However, it doesn’t make compound operations (like count++) atomic, so race conditions can still occur.

AtomicInteger provides both atomicity and visibility, using lock-free CAS (Compare-And-Set) operations internally. It’s ideal for thread-safe counters or accumulators.

In short:

  • Use volatile for flags or simple state visibility.
  • Use AtomicInteger (or LongAdder) for shared counters or concurrent updates.

6. ๐Ÿ’ก Thread Safety by Design

Instead of relying heavily on locks, a modern approach to concurrency is to design for thread safety — avoid shared mutable state altogether. This makes code simpler, safer, and far easier to reason about under concurrent execution.

  • Use immutable objects — once created, their state never changes.
  • Apply thread confinement — each thread owns its data.
  • Use message passing (e.g., queues, Kafka) to communicate safely between threads.
  • Leverage high-level abstractions like CompletableFuture or reactive streams.
// Example 1: Immutable design — inherently thread-safe
public final class Order {
    private final String id;
    private final BigDecimal amount;

    public Order(String id, BigDecimal amount) {
        this.id = id;
        this.amount = amount;
    }

    public Order applyDiscount(BigDecimal discount) {
        return new Order(id, amount.subtract(discount));
    }

    public BigDecimal getAmount() {
        return amount;
    }
}

✅ No synchronization needed — immutable objects are inherently thread-safe

// Example 2: Thread confinement — each thread keeps local state
ExecutorService executor = Executors.newFixedThreadPool(2);
List<Future<Integer>> futures = new ArrayList<>();

for (int i = 0; i < 2; i++) {
    futures.add(executor.submit(() -> {
        int localCount = 0;
        for (int j = 0; j < 1000; j++) localCount++;
        return localCount;
    }));
}

int total = 0;
for (Future<Integer> f : futures) total += f.get();

System.out.println("Final count: " + total);
executor.shutdown();

✅ Each thread works on local data — no contention or shared state

❌ Requires architectural foresight — can’t always retrofit into existing designs

Designing for thread safety upfront prevents entire classes of concurrency bugs and allows developers to focus on logic rather than synchronization details.

7. ๐Ÿ’พ Thread Diagnostics & Memory Issues

  • Thread dump: Snapshot of all threads.
  • Memory leaks: Objects remain in heap but unused. Causes:
    • Static references
    • Unclosed resources (streams, connections)
  • Tools: JProfiler, VisualVM, JConsole, Flight Recorder (JFR)

8. ๐Ÿ“ Summary Table of Thread Pools

Thread Pool Behavior Use Case
newFixedThreadPool(int n) Fixed threads. Tasks queued if busy. Predictable concurrency
newCachedThreadPool() Creates threads on demand, reuses idle ones. Short-lived tasks, bursts
newSingleThreadExecutor() Single worker thread sequential execution. Ordered tasks
newScheduledThreadPool(int n) Execute tasks periodically or with delay. Recurring/scheduled tasks
newVirtualThreadPerTaskExecutor() Lightweight virtual threads, thousands possible. High concurrency, scalable microservices

9. ๐Ÿ”— Modern Java Utilities

  • CompletableFuture: Async computation, combine multiple futures.
  • ForkJoinPool: Efficient for parallel streams / divide-and-conquer.
  • Structured Concurrency (Java 21+): Manage multiple tasks as a single unit with StructuredTaskScope.
  • Volatile & transient: Volatile for visibility, transient for skipping serialization.

9. ๐Ÿ“Š Visual Summary

Thread

Platform vs Virtual Thread

Deadlock

Thread waiting forever

Race

Shared data conflict

Want to learn more about thread-safe counters and locks? Check out Thread-Safe Programming in Java: Locks, Atomic Variables & LongAdder.

Labels: Java, Threads, Concurrency, Executor, Synchronization, Deadlock, Race Condition, Virtual Threads, Structured Concurrency, Thread Safety, Immutable Design

Comments

Popular posts from this blog

๐Ÿ› ️ The Code Hut - Index

๐Ÿ›ก️ Resilience Patterns in Distributed Systems

๐Ÿ›ก️ Thread-Safe Programming in Java: Locks, Atomic Variables & LongAdder