Understanding Java Concurrency: Executor, ExecutorService, and Beyond

Concurrency in Java enables multiple threads to run in parallel, allowing applications to utilize CPU cores efficiently and improve throughput. This guide explores Java concurrency — from the basic Executor interface to advanced constructs like ThreadPoolExecutor, ScheduledExecutorService, and concurrency utilities like CountDownLatch, CyclicBarrier, and more.


1. Executor vs. ExecutorService

Executor

The simplest interface for task execution:

    void execute(Runnable command);

Executes the command asynchronously using an implementation-defined strategy.


ExecutorService

More advanced than Executor, it supports:

  • Lifecycle management

  • Task submission with results (Future)

  • Shutdown and termination

<T> Future<T> submit(Callable<T> task);
void shutdown();
boolean awaitTermination(long timeout, TimeUnit unit);

2. ⚙️ ThreadPoolExecutor: Fine-Grained Control

Why use ThreadPoolExecutor?

Gives you full control over:

  • Thread counts

  • Queuing behavior

  • Rejection policies

Constructor:

ThreadPoolExecutor( int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue )

Parameters:

ParameterDescription
corePoolSizeMinimum number of threads kept alive
maximumPoolSizeMaximum threads allowed
keepAliveTimeIdle time for non-core threads
workQueueQueues tasks before they are executed

🔁 Execution Flow:
  1. If current threads < corePoolSize, a new thread is created.

  2. If the core pool is full, the task is added to the queue.

  3. If the queue is full and current threads < maximumPoolSize, a new thread is added.

  4. If all full → task is rejected.


3. 🧱 BlockingQueue in ThreadPoolExecutor

The BlockingQueue is essential to handle task queuing:

  • Avoids thread explosion

  • Provides backpressure

  • Ensures predictable behavior under load

Common Implementations:

  • LinkedBlockingQueue (unbounded)

  • ArrayBlockingQueue (bounded)

  • SynchronousQueue (direct hand-off, no queueing)


4. 🕰️ ScheduledThreadPoolExecutor

For running recurring or delayed tasks:

ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2); scheduler.scheduleAtFixedRate(() -> { System.out.println("Runs every 10 seconds"); }, 0, 10, TimeUnit.SECONDS);

5. 🧰 Common Factory Methods in Executors Class

The Executors utility class provides easy-to-use factory methods to create various types of thread pools.

Executors.newSingleThreadExecutor()

ExecutorService executor = Executors.newSingleThreadExecutor();

Runs tasks sequentially using a single thread.


Executors.newFixedThreadPool(int n)

ExecutorService executor = Executors.newFixedThreadPool(5);

Reuses a fixed number of threads.


Executors.newCachedThreadPool()

ExecutorService executor = Executors.newCachedThreadPool();

Creates new threads as needed and reuses idle ones.


Executors.newScheduledThreadPool(int n)

ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);

Used for scheduling periodic/delayed tasks.


Executors.newSingleThreadScheduledExecutor()

ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();

A single-thread version of the scheduled pool.


Executors.newWorkStealingPool(int parallelism)

ExecutorService executor = Executors.newWorkStealingPool();

Uses the ForkJoinPool with multiple worker threads.


MethodThread BehaviorUse Case
newSingleThreadExecutor()1 threadSequential task flow
newFixedThreadPool(n)Fixed-size threadsStable load
newCachedThreadPool()Unlimited threadsLightweight tasks
newScheduledThreadPool(n)Scheduled thread poolPeriodic tasks
newSingleThreadScheduledExecutor()1 scheduled threadSerial periodic jobs
newWorkStealingPool()ForkJoin threadsCPU-bound parallelism

6. Runnable vs. Callable

FeatureRunnableCallable<V>
Returns value?
Checked exceptions?
Submit methodexecute(), submit()submit()

7. Producer-Consumer with BlockingQueue

BlockingQueue<String> queue = new LinkedBlockingQueue<>(10); queue.put("data"); // Producer queue.take(); // Consumer

Handles concurrency safely using queue-based coordination.


8. SynchronizedMap vs. ConcurrentHashMap

FeatureSynchronizedMapConcurrentHashMap
Lock granularityWhole mapBucket-level
Iteration safetyNo (ConcurrentModificationException)Yes


9. CountDownLatch

CountDownLatch latch = new CountDownLatch(3); latch.await(); // Waits until count is 0 latch.countDown(); // Called by threads when done

10. CyclicBarrier

CyclicBarrier barrier = new CyclicBarrier(3);
barrier.await(); // All threads must call this to proceed

11. CountDownLatch vs. CyclicBarrier

FeatureCountDownLatchCyclicBarrier
Reusability❌ One-time use✅ Can reset
WaitingOne thread waitsAll threads wait

12. Deadlock

When threads wait on each other’s locks, creating a cycle.

Solution:

  • Lock ordering

  • Use tryLock()

  • Reduce lock scope


13. Race Condition

Occurs when threads access shared state unsafely.

Solution:

  • Use synchronized

  • Use atomic classes (e.g., AtomicInteger)


14. Semaphore

Controls concurrent access to a resource:

Semaphore sem = new Semaphore(2); sem.acquire(); // Get access sem.release(); // Release

15. ForkJoin Framework

Designed for recursive task decomposition:

ForkJoinPool pool = new ForkJoinPool(); Integer result = pool.invoke(new MyRecursiveTask());

✅ Summary

Mastering Java’s concurrency tools improves scalability and system utilization. Choose the right ExecutorService based on your workload and use concurrency utilities to coordinate thread behavior.


🔗 Further Resources:


Comments