⚡ 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();
Runnable task = () -> System.out.println("Runnable running");
new Thread(task).start();
Callable<Integer> task = () -> 42; Future<Integer> future = Executors.newSingleThreadExecutor().submit(task); System.out.println(future.get());
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
int counter = 0; Runnable r = () -> counter++; // multiple threads increment → unexpected results
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 supportsConditionobjects 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
CompletableFutureor 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
Platform vs Virtual Thread
Thread waiting forever
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
Post a Comment