Popular Searches
Popular Course Categories
Popular Courses

Understanding Deadlocks, Thread Safety, and ExecutorService Fundamentals

Understanding Deadlocks, Thread Safety, and ExecutorService Fundamentals

Multithreading and Concurrency Fundamentals

Understanding Deadlocks, Thread Safety, and ExecutorService Fundamentals

Course: Core Java Training

Topic: Deadlocks, Thread Safety, and ExecutorService Fundamentals

Course Link: Core Java Training Course

Demo Link: Register for a Course Demo

JustAcademy Website: Visit JustAcademy


1. Introduction to Java Concurrency

Java concurrency allows multiple tasks to make progress during overlapping periods of execution. It is an important concept in modern application development because applications frequently perform database operations, process requests, access shared resources, and execute background tasks simultaneously.

Java provides multithreading support through the Thread class, the Runnable and Callable interfaces, synchronization mechanisms, and the java.util.concurrent package.

However, concurrent programming introduces challenges. Multiple threads may access the same data, compete for locks, or wait for resources held by one another. Developers must understand deadlocks and thread safety to avoid these problems.

The ExecutorService API provides a higher-level way to manage task execution through thread pools, task submission, result retrieval, cancellation, and controlled shutdown.

Learning Objectives

  • Understand Java concurrency and multithreading fundamentals.
  • Define deadlock and identify its necessary conditions.
  • Learn how deadlocks occur and how to prevent them.
  • Understand thread safety and shared mutable state.
  • Use synchronization, atomic variables, and concurrent collections appropriately.
  • Understand the Executor framework and thread pools.
  • Use Runnable, Callable, Future, and ExecutorService.
  • Manage task execution, exceptions, cancellation, and executor shutdown.
  • Apply concurrency concepts to practical Java applications.

2. Understanding Threads and Concurrent Execution

A thread is an independent path of execution within a Java process. Multiple threads can execute different tasks concurrently while sharing process resources, including heap objects.

Example: Creating Threads

class MessageTask implements Runnable {
    @Override
    public void run() {
        System.out.println(
            Thread.currentThread().getName()
            + " is executing"
        );
    }
}

public class ThreadExample {
    public static void main(String[] args) {
        Thread t1 = new Thread(new MessageTask());
        Thread t2 = new Thread(new MessageTask());

        t1.start();
        t2.start();
    }
}

Explanation

  • Runnable represents a task that does not return a result.
  • The run() method contains the task logic.
  • The Thread constructor receives the task.
  • The start() method starts a new thread of execution.
  • The execution order of the two threads is not guaranteed.

Concurrent execution can improve responsiveness and throughput. However, if threads access shared mutable data without suitable coordination, the application may produce incorrect results.


3. What Is a Deadlock?

A deadlock is a situation in which two or more threads become permanently blocked because each is waiting for a resource or lock held by another thread in the group.

Java does not automatically prevent every deadlock. Developers must design their locking strategies carefully to avoid circular dependencies.

Real-World Example

Imagine two people working with two shared resources:

  • Person A holds Resource 1 and waits for Resource 2.
  • Person B holds Resource 2 and waits for Resource 1.
  • Neither person releases the resource they already hold.
  • Both remain waiting indefinitely.

The same situation can occur when Java threads acquire multiple object monitors in conflicting orders.

Deadlock Representation

Thread 1:
    Acquires Lock A
    Waits for Lock B

Thread 2:
    Acquires Lock B
    Waits for Lock A

Result:
    Thread 1 waits for Thread 2
    Thread 2 waits for Thread 1
    Neither thread can proceed

4. Four Necessary Conditions for Deadlock

Deadlock theory identifies four conditions that must hold simultaneously for a classic resource-allocation deadlock to occur.

4.1 Mutual Exclusion

A resource can be held by only one thread at a time. For example, only one thread can own a particular Java monitor lock at any given moment.

4.2 Hold and Wait

A thread holds at least one resource while waiting to acquire another resource held by a different thread.

4.3 No Preemption

A resource cannot simply be taken away from a thread by another thread. The holder must release the resource voluntarily under the applicable locking mechanism.

4.4 Circular Wait

A circular chain of waiting exists. For example, Thread A waits for a lock held by Thread B, while Thread B waits for a lock held by Thread A.

Condition Meaning Possible Prevention Strategy
Mutual exclusion Resources are exclusively owned Use nonexclusive access where the resource and requirements permit it
Hold and wait A thread holds one resource while requesting another Acquire required resources together where practical or avoid nested locking
No preemption Resources are not forcibly taken from their holders Use interruptible or timed lock acquisition when suitable
Circular wait Threads wait in a cycle Acquire locks in a consistent global order

In practical Java applications, consistent lock ordering is one of the most useful ways to reduce deadlock risk.


5. Deadlock Example in Java

The following program demonstrates a possible deadlock caused by two threads acquiring two locks in opposite orders.

public class DeadlockExample {
    private static final Object lockA = new Object();
    private static final Object lockB = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (lockA) {
                System.out.println("Thread 1 acquired Lock A");

                sleepBriefly();

                synchronized (lockB) {
                    System.out.println("Thread 1 acquired Lock B");
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (lockB) {
                System.out.println("Thread 2 acquired Lock B");

                sleepBriefly();

                synchronized (lockA) {
                    System.out.println("Thread 2 acquired Lock A");
                }
            }
        });

        t1.start();
        t2.start();
    }

    private static void sleepBriefly() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

Possible Output

Thread 1 acquired Lock A
Thread 2 acquired Lock B

The program may stop making progress after these messages if each thread obtains its first lock before trying to acquire the second. The precise behavior depends on thread scheduling.

Why Does Deadlock Occur?

  1. Thread 1 acquires lockA.
  2. Thread 2 acquires lockB.
  3. Thread 1 requests lockB, which Thread 2 holds.
  4. Thread 2 requests lockA, which Thread 1 holds.
  5. Both threads remain blocked while waiting for the other lock.

Important: The sleep call makes the problematic interleaving easier to observe but does not guarantee that deadlock will occur in every execution.


6. How to Prevent Deadlocks

6.1 Use a Consistent Lock Order

The simplest strategy is to acquire multiple locks in the same order across all code paths.

public class SafeLockExample {
    private static final Object lockA = new Object();
    private static final Object lockB = new Object();

    public static void performTask() {
        synchronized (lockA) {
            synchronized (lockB) {
                System.out.println("Task completed safely");
            }
        }
    }
}

Any other method requiring both locks should also acquire lockA before lockB. Merely changing one method is insufficient if another method still uses the opposite order.

6.2 Avoid Unnecessary Nested Locks

Reduce the number of locks a thread must hold simultaneously. Where possible, complete independent operations separately instead of nesting multiple synchronized sections.

6.3 Use tryLock() with a Timeout

The ReentrantLock class supports timed and interruptible lock acquisition. If a thread cannot acquire a required lock within a chosen timeout, it can release locks already acquired and retry or report failure.

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class TryLockExample {
    private final ReentrantLock lock = new ReentrantLock();

    public void performTask() throws InterruptedException {
        if (lock.tryLock(2, TimeUnit.SECONDS)) {
            try {
                System.out.println("Lock acquired");
            } finally {
                lock.unlock();
            }
        } else {
            System.out.println("Could not acquire lock");
        }
    }
}

This example demonstrates timed acquisition of a single lock. Using timeouts for multiple locks requires careful cleanup of every lock already acquired; a timeout alone does not guarantee deadlock-free design.

6.4 Avoid Holding Locks During Slow Operations

Avoid performing network calls, database operations, or long-running tasks while holding locks unless the design specifically requires it. Long-held locks increase contention and can make lock cycles more likely.

6.5 Use Interruption and Cancellation Responsibly

Interruptible operations allow tasks to respond to cancellation requests. However, interrupting a thread does not automatically release every monitor or solve a deadlock involving intrinsic locks.


7. How to Detect and Debug Deadlocks

Deadlocks can be difficult to identify because a program may appear frozen without throwing an exception.

Useful Debugging Techniques

  • Take a Java thread dump when the application stops making progress.
  • Inspect threads in the BLOCKED state and identify the monitors they are waiting to acquire.
  • Check which threads own the required locks.
  • Use tools such as jstack or Java monitoring tools to inspect thread activity.
  • Review code paths that acquire multiple locks in different orders.
  • Use timeouts and diagnostic logging around lock acquisition where appropriate.
  • Reproduce the issue with concurrent tests and realistic workloads.

Example Diagnostic Command

jstack <process-id>

Replace <process-id> with the Java application's process ID. Depending on the JVM and diagnostic output, a thread dump may identify a deadlock and show the involved threads and locks.


8. What Is Thread Safety?

Thread safety means that a class, method, or component behaves correctly when accessed concurrently by multiple threads according to its documented contract.

A thread-safe component protects its internal state against invalid concurrent interactions. The appropriate strategy depends on how the data is shared and what operations must remain consistent.

Examples of Thread-Safe Design

  • Using synchronized methods to protect shared mutable state.
  • Using atomic classes for individual atomic updates.
  • Using concurrent collections for shared data structures.
  • Designing immutable objects that cannot change after construction.
  • Keeping mutable data local to one thread.

Why Thread Safety Matters

  • Prevents lost updates and inconsistent state.
  • Helps maintain application correctness under concurrent load.
  • Reduces unpredictable failures that are difficult to reproduce.
  • Supports reliable background processing and concurrent request handling.
  • Improves confidence in shared components used across an application.

9. Thread Safety and Shared Mutable Data

Shared mutable data is data that can be accessed by multiple threads and changed during execution. It is a common source of concurrency bugs.

Unsafe Example

class UnsafeCounter {
    private int count = 0;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

The increment() method is unsafe when several threads call it concurrently on the same object. The ordinary increment operation is not atomic.

Thread-Safe Version Using synchronized

class SafeCounter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

The synchronized methods use the same object's monitor. This protects the increment operation and coordinates reads through the same monitor.

Thread-Safe Version Using AtomicInteger

import java.util.concurrent.atomic.AtomicInteger;

class AtomicCounter {
    private final AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet();
    }

    public int getCount() {
        return count.get();
    }
}

AtomicInteger is useful for independent integer updates. For operations involving multiple variables or a sequence of dependent steps, additional coordination may still be necessary.


10. Strategies for Achieving Thread Safety

10.1 Synchronization

Use the synchronized keyword to protect critical sections guarded by the same monitor.

public synchronized void updateBalance(double amount) {
    balance += amount;
}

10.2 Atomic Classes

Atomic classes provide operations on individual values that are performed atomically.

AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();

10.3 Immutable Objects

An immutable object cannot have its state changed after construction. Properly designed immutable objects are naturally easier to share safely across threads.

final class Employee {
    private final String name;
    private final int id;

    public Employee(String name, int id) {
        this.name = name;
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public int getId() {
        return id;
    }
}

This class has final fields and no mutator methods. For objects containing references to mutable data, immutability may also require defensive copying and avoiding exposure of mutable internals.

10.4 Concurrent Collections

Concurrent collections are designed for safe concurrent access. Examples include ConcurrentHashMap and BlockingQueue.

import java.util.concurrent.ConcurrentHashMap;

ConcurrentHashMap<String, Integer> scores =
        new ConcurrentHashMap<>();

scores.put("Java", 95);
scores.put("SQL", 90);

Concurrent collections protect their own operations, but a sequence of multiple operations may still require additional coordination if the sequence must be atomic.

10.5 Thread Confinement

Thread confinement means that mutable data is accessed by only one thread. Avoiding shared mutable state often eliminates the need for synchronization.

10.6 Volatile Variables

The volatile keyword provides visibility and ordering guarantees for a field across threads. It does not make compound operations such as count++ atomic and does not provide mutual exclusion.


11. What Is the Executor Framework?

The Executor framework is part of Java's java.util.concurrent package. It separates task submission from the details of how tasks are executed.

Instead of manually creating a new Thread for every task, an application can submit tasks to an executor that manages execution resources.

Main Benefits

  • Separates task logic from thread management.
  • Supports reusable worker threads through thread pools.
  • Allows tasks to be queued and executed concurrently.
  • Supports returning results through Future.
  • Provides task cancellation and lifecycle management.
  • Can improve resource management when configured appropriately.

The Java concurrency APIs provide executor implementations, scheduling support, futures, and other coordination utilities. Read the Java concurrency API documentation.


12. Understanding Executor, ExecutorService, and ScheduledExecutorService

Interface Purpose Common Methods
Executor Defines a basic task-execution abstraction execute()
ExecutorService Supports task submission, result tracking, and controlled shutdown submit(), invokeAll(), shutdown()
ScheduledExecutorService Supports delayed and periodic task execution schedule(), scheduleAtFixedRate()

Executor

The Executor interface provides the execute(Runnable) method. It defines how a task is submitted without requiring the caller to manage the underlying thread directly.

ExecutorService

ExecutorService extends Executor and adds methods for submitting tasks, retrieving results, coordinating task completion, and shutting down the service.

ScheduledExecutorService

ScheduledExecutorService extends ExecutorService and supports tasks that run after a delay or periodically.


13. What Is a Thread Pool?

A thread pool is a collection of worker threads that execute submitted tasks. Instead of creating a new thread for every operation, an executor can reuse available workers.

How a Thread Pool Works

  1. The application creates an executor.
  2. The application submits tasks to the executor.
  3. The executor assigns tasks to available worker threads.
  4. If all workers are busy, additional tasks may wait in a queue or be rejected, depending on the executor configuration.
  5. When a worker finishes a task, it can process another task.
  6. The application shuts down the executor when it is no longer needed.

Advantages of Thread Pools

  • Reduces repeated thread-creation overhead.
  • Allows control over the number of concurrently executing tasks.
  • Provides a centralized place to manage task execution.
  • Supports queued work and task completion tracking.
  • Can improve application responsiveness and resource utilization.

A thread pool does not automatically make task code thread-safe. If multiple tasks modify the same object, the code must still protect that shared state.


14. Creating an ExecutorService

The Executors utility class provides factory methods for creating commonly used executor implementations.

Common Factory Methods

Method Description Typical Use
newFixedThreadPool(n) Creates a pool with a fixed number of worker threads Controlling concurrent worker count
newSingleThreadExecutor() Uses one worker to execute tasks sequentially Serial task processing
newCachedThreadPool() Creates and reuses threads as needed Short-lived asynchronous tasks with suitable workload controls
newScheduledThreadPool(n) Creates a scheduler with a core pool size Delayed and periodic jobs

Resource consideration: Some factory-created executors use unbounded queues or allow substantial thread growth. For production workloads with unpredictable task volumes, consider a deliberately configured ThreadPoolExecutor with suitable queue capacity, thread limits, and rejection handling.


15. Example: Using ExecutorService with Runnable

The Runnable interface represents a task that does not return a result.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorRunnableDemo {
    public static void main(String[] args) {
        ExecutorService executor =
                Executors.newFixedThreadPool(2);

        try {
            Runnable task1 = () ->
                System.out.println(
                    "Task 1: " +
                    Thread.currentThread().getName()
                );

            Runnable task2 = () ->
                System.out.println(
                    "Task 2: " +
                    Thread.currentThread().getName()
                );

            executor.execute(task1);
            executor.execute(task2);
        } finally {
            executor.shutdown();
        }
    }
}

Explanation

  • A fixed thread pool with two workers is created.
  • Two Runnable tasks are defined.
  • The execute() method submits each task for execution.
  • The worker threads run the tasks independently.
  • shutdown() stops the executor from accepting new tasks while allowing already submitted tasks to complete.

The output order is not guaranteed. The executor may reuse worker threads for multiple tasks.


16. Understanding Callable and Future

Java provides Callable for tasks that return a result and may throw checked exceptions. The Future interface represents the result of an asynchronous computation.

Runnable vs. Callable

Feature Runnable Callable
Return value No result from the task method Returns a value of type V
Method run() call()
Checked exceptions Cannot declare arbitrary checked exceptions in run() Can declare checked exceptions
Common submission method execute() or submit() submit()
Result tracking Via a Future when submitted using a suitable submit method Typically through a Future returned by submit

Example: Callable and Future

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class CallableFutureDemo {
    public static void main(String[] args)
            throws Exception {

        ExecutorService executor =
                Executors.newSingleThreadExecutor();

        try {
            Callable<Integer> task = () -> {
                int a = 20;
                int b = 30;
                return a + b;
            };

            Future<Integer> future =
                    executor.submit(task);

            Integer result = future.get();

            System.out.println("Result: " + result);
        } finally {
            executor.shutdown();
        }
    }
}

Output

Result: 50

Explanation

  • The Callable task calculates the sum of two numbers.
  • submit() schedules the task and returns a Future.
  • future.get() waits if necessary for completion and retrieves the result.
  • If the task fails, get() reports the failure through an ExecutionException.
  • The executor is shut down in the finally block.

Important: Calling get() without a timeout can wait indefinitely if the task never completes. Use a timed get() when the application requires a deadline.


17. Important ExecutorService Methods

Method Purpose
execute(Runnable) Submits a Runnable for execution without returning a Future
submit(Runnable) Submits a Runnable and returns a Future representing completion
submit(Callable) Submits a result-producing task and returns a Future
invokeAll() Submits a collection of tasks and waits for all to finish, subject to the applicable timeout or interruption
invokeAny() Returns the successful result of one completed task, or throws if no task succeeds
shutdown() Stops accepting new tasks and allows submitted tasks to finish
shutdownNow() Attempts to stop active tasks and returns tasks that have not started
awaitTermination() Waits for termination for a specified period
isShutdown() Checks whether shutdown has been initiated
isTerminated() Checks whether all tasks have completed after shutdown

18. ExecutorService Shutdown and Lifecycle Management

Executors use resources that must be managed carefully. A service that is no longer needed should be shut down so its worker threads and other resources can be released.

Graceful Shutdown Example

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class ExecutorShutdownDemo {
    public static void main(String[] args)
            throws InterruptedException {

        ExecutorService executor =
                Executors.newFixedThreadPool(2);

        try {
            for (int i = 1; i <= 5; i++) {
                final int taskId = i;

                executor.submit(() ->
                    System.out.println(
                        "Processing task " + taskId
                    )
                );
            }
        } finally {
            executor.shutdown();
        }

        try {
            if (!executor.awaitTermination(
                    5, TimeUnit.SECONDS)) {
                executor.shutdownNow();

                if (!executor.awaitTermination(
                        5, TimeUnit.SECONDS)) {
                    System.err.println(
                        "Executor did not terminate"
                    );
                }
            }
        } catch (InterruptedException e) {
            executor.shutdownNow();
            Thread.currentThread().interrupt();
        }
    }
}

Explanation

  • Five tasks are submitted to a pool of two workers.
  • shutdown() rejects new tasks but allows accepted tasks to complete.
  • awaitTermination() waits for the executor to terminate for up to the specified duration.
  • shutdownNow() makes a best-effort attempt to stop active work and returns tasks that never commenced.
  • Interruption is handled by requesting shutdown and restoring the current thread's interrupt status.

Note: shutdownNow() does not forcibly terminate arbitrary Java code. Tasks need to cooperate with interruption or another cancellation mechanism for prompt termination.


19. Exception Handling with ExecutorService

Task exceptions must be handled according to how the task is submitted.

Using submit()

When a task is submitted using submit(), its failure is generally captured by the returned Future. Calling get() reports the failure through an ExecutionException.

import java.util.concurrent.*;

public class FutureExceptionDemo {
    public static void main(String[] args) {
        ExecutorService executor =
                Executors.newSingleThreadExecutor();

        try {
            Future<Integer> future =
                    executor.submit(() -> {
                        throw new IllegalStateException(
                            "Task failed"
                        );
                    });

            try {
                System.out.println(future.get());
            } catch (ExecutionException e) {
                System.out.println(
                    "Task error: " + e.getCause()
                );
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        } finally {
            executor.shutdown();
        }
    }
}

Important Exception Types

  • ExecutionException: Indicates that a task failed while retrieving its result.
  • InterruptedException: Indicates that a waiting operation was interrupted.
  • CancellationException: Indicates that the Future was cancelled.
  • RejectedExecutionException: May occur when a task cannot be accepted by the executor.

20. Task Cancellation and Timeouts

Long-running tasks should have appropriate cancellation and timeout strategies, especially when they involve external services or potentially blocking operations.

Example: Timed Result Retrieval

import java.util.concurrent.*;

public class FutureTimeoutDemo {
    public static void main(String[] args) {
        ExecutorService executor =
                Executors.newSingleThreadExecutor();

        Future<String> future = executor.submit(() -> {
            Thread.sleep(3000);
            return "Completed";
        });

        try {
            System.out.println(
                future.get(1, TimeUnit.SECONDS)
            );
        } catch (TimeoutException e) {
            System.out.println("Task is taking too long");
            future.cancel(true);
        } catch (InterruptedException e) {
            future.cancel(true);
            Thread.currentThread().interrupt();
        } catch (ExecutionException e) {
            System.out.println("Task failed");
        } finally {
            executor.shutdownNow();
        }
    }
}

Explanation

  • The task sleeps for three seconds before returning a result.
  • The main thread waits for at most one second for the result.
  • If the timeout expires, the Future can be cancelled.
  • cancel(true) requests interruption of the task if it is running.
  • The task must respond appropriately to interruption for cancellation to work promptly.

21. ExecutorService vs. Creating Threads Manually

Feature Manual Thread Creation ExecutorService
Task submission Usually requires creating and starting Thread objects Submit tasks to an executor
Thread reuse Must be designed and managed separately Thread pools can reuse workers
Result retrieval Requires additional coordination Future supports result retrieval
Task management Mostly application-managed Provides task submission and lifecycle APIs
Concurrency limits Must be implemented separately Can be configured through suitable executor implementations
Shutdown management Manage thread lifecycle manually Offers shutdown and termination methods

ExecutorService is generally a convenient choice for managing collections of concurrent tasks. Manual thread creation remains useful for learning thread fundamentals and for specialized use cases.


22. Thread Safety and ExecutorService: Important Differences

ExecutorService manages task execution; it does not automatically protect shared application data.

class SharedCounter {
    private int count = 0;

    public void increment() {
        count++;
    }
}

Submitting calls to this class through an executor does not make its increment() method thread-safe.

Safer Version

import java.util.concurrent.atomic.AtomicInteger;

class SharedCounter {
    private final AtomicInteger count = new AtomicInteger();

    public void increment() {
        count.incrementAndGet();
    }

    public int getCount() {
        return count.get();
    }
}

The atomic counter supports concurrent increments safely. In real applications, choose a concurrency strategy based on the complete operation being protected, not just on the fact that tasks execute through a thread pool.


23. Common Concurrency Problems

Problem Description Possible Solution
Deadlock Threads wait indefinitely for locks held by one another Consistent lock ordering and careful lock design
Race condition Results depend on timing of concurrent operations Synchronization, atomic operations, or other suitable coordination
Starvation A thread repeatedly fails to obtain resources or execution time Review scheduling, lock fairness, and critical-section duration
Livelock Threads remain active but fail to make useful progress Change retry policies or use backoff strategies
Excessive contention Many threads compete for the same resource Reduce shared state and shorten critical sections
Resource exhaustion Too many threads or queued tasks consume available resources Use suitable thread limits, queue capacities, and rejection policies

24. Best Practices for Java Concurrency

  1. Identify all shared mutable state before implementing concurrent tasks.
  2. Prefer immutable objects and thread confinement when possible.
  3. Protect complete logical operations using a consistent locking strategy.
  4. Acquire multiple locks in a consistent order.
  5. Keep critical sections short and avoid unnecessary nested locks.
  6. Use atomic classes for appropriate individual atomic operations.
  7. Choose concurrent collections when their behavior fits the use case.
  8. Use ExecutorService instead of manually creating large numbers of threads for routine task management.
  9. Configure thread pools according to workload, available resources, and latency requirements.
  10. Handle task exceptions and interruption correctly.
  11. Always establish an appropriate executor shutdown strategy.
  12. Use timeouts for operations that might otherwise wait indefinitely.
  13. Test concurrency behavior under realistic load and varied scheduling conditions.
  14. Use thread dumps and monitoring tools to investigate hangs and contention.

25. Hands-On Practice Exercises

Exercise 1: Create a Deadlock

Create two shared lock objects and two threads that acquire them in opposite orders. Observe the possible deadlock and explain how circular wait occurs.

Exercise 2: Prevent Deadlock

Modify the previous program so that both threads acquire locks in the same order. Run the program repeatedly to verify that the lock-ordering problem has been removed.

Exercise 3: Build a Thread-Safe Counter

Implement a counter using an ordinary integer, a synchronized method, and AtomicInteger. Run multiple tasks and compare the results.

Exercise 4: Execute Tasks with ExecutorService

Create a fixed thread pool and submit ten Runnable tasks. Print each task identifier and worker thread name.

Exercise 5: Use Callable and Future

Submit a Callable that returns a calculation result. Retrieve the result using Future and handle possible exceptions.

Exercise 6: Manage Executor Shutdown

Submit multiple tasks, call shutdown, and use awaitTermination to wait for completion. Add a fallback cancellation strategy.

Exercise 7: Handle Task Failure

Submit a task that throws an exception. Observe how the exception is reported when retrieving its Future result.

Exercise 8: Implement a Task Timeout

Submit a task that takes several seconds to finish. Use timed Future.get and implement a cancellation policy.


26. Interview Questions and Answers

Q1. What is a deadlock in Java?

Answer: A deadlock occurs when two or more threads wait indefinitely for resources or locks held by one another, preventing progress.

Q2. What are the four necessary conditions for a classic deadlock?

Answer: Mutual exclusion, hold and wait, no preemption, and circular wait.

Q3. How can deadlocks be prevented?

Answer: Use a consistent lock order, reduce nested locking, consider timed or interruptible acquisition where suitable, and avoid holding locks unnecessarily.

Q4. What is thread safety?

Answer: Thread safety means a component maintains correct behavior when accessed concurrently according to its contract.

Q5. Does ExecutorService automatically make code thread-safe?

Answer: No. ExecutorService manages task execution, but shared mutable state must still be protected appropriately.

Q6. What is the Executor framework?

Answer: It is a Java concurrency framework that separates task submission from the details of task execution and thread management.

Q7. What is the difference between Executor and ExecutorService?

Answer: Executor provides basic task execution through execute(). ExecutorService adds task submission, Future-based result tracking, and lifecycle management.

Q8. What is a thread pool?

Answer: A thread pool manages worker threads that can be reused to execute multiple submitted tasks.

Q9. What is the difference between Runnable and Callable?

Answer: Runnable's run() method does not return a task result. Callable's call() method returns a value and can throw checked exceptions.

Q10. What is a Future in Java?

Answer: Future represents the result of an asynchronous computation and provides methods to retrieve a result, check completion, or request cancellation.

Q11. What is the difference between shutdown() and shutdownNow()?

Answer: shutdown() stops accepting new tasks and allows submitted tasks to finish. shutdownNow() attempts to stop active tasks and returns tasks that have not started.

Q12. What is AtomicInteger?

Answer: AtomicInteger provides atomic operations on a single integer value and is useful for concurrent counters.

Q13. Can a synchronized program still experience deadlock?

Answer: Yes. Synchronization protects critical sections, but acquiring multiple locks in an inconsistent order can cause deadlock.

Q14. What is starvation?

Answer: Starvation occurs when a thread repeatedly fails to obtain the resources or execution time needed to make progress.

Q15. Why should an ExecutorService be shut down?

Answer: Shutdown prevents new task submissions and allows executor resources to be released after existing work is handled.


27. Quick Revision Checklist

  • Deadlock occurs when threads wait for resources held by one another.
  • The four classic deadlock conditions are mutual exclusion, hold and wait, no preemption, and circular wait.
  • Consistent lock ordering is a key deadlock-prevention strategy.
  • Thread safety ensures correct behavior under concurrent access.
  • Shared mutable state requires an appropriate concurrency strategy.
  • Synchronization and atomic classes can help prevent unsafe concurrent updates.
  • Executor separates task submission from execution details.
  • ExecutorService supports task submission, Futures, and lifecycle management.
  • Thread pools reuse workers to execute multiple tasks.
  • Runnable represents a task without a result; Callable can return a result.
  • Future supports result retrieval, completion checks, and cancellation requests.
  • shutdown allows submitted tasks to complete; shutdownNow attempts to stop ongoing work.
  • ExecutorService does not automatically make shared application state thread-safe.
  • Concurrency testing, monitoring, and careful resource management are essential.

28. Conclusion

Deadlocks, thread safety, and ExecutorService are important topics in Core Java because they determine how reliably an application behaves when several tasks execute concurrently.

Deadlocks can prevent an application from making progress, while unsafe access to shared mutable data can produce inconsistent results. Developers can reduce these risks by applying consistent lock ordering, protecting shared state correctly, using immutable data where practical, and choosing suitable concurrency utilities.

ExecutorService simplifies task management by providing reusable worker threads, task submission, result tracking, cancellation, and controlled shutdown. However, good concurrency design still requires developers to manage shared state, exceptions, timeouts, and resource usage carefully.

Practice these concepts with counters, banking examples, multiple locks, and executor-based tasks to develop a strong foundation in Java multithreading.


Learn Core Java with JustAcademy

Continue learning Java programming and multithreading through these resources:

Previous
whatsapp