Java Concurrency Fundamentals and Implementation

Understanding the basic concepts of programs, processes, and threads is fundamental to grasping multithreading in Java.

Definitions

  • Program: A static set of instructions written in a specific language to accomplish a particular task. Examples include applications like Excel or Word.
  • Process: An instance of a program in execution. It's a dynamic entity with its own lifecycle, including creation, existence, and termination.
  • Thread: A thread is a single execution path within a process. A process can be further divided into multiple threads, each capable of executing independently.

Elaborated Definitions

  • Process: Defined by Wikipedia as the execution of a program on a given dataset, a process is the fundamental unit for resource allocation and scheduling in an operating system. In modern thread-oriented architectures, processes serve as containers for threads. A program is a description of instructions and data, while a process is the active entity.
  • Thread: The smallest unit of execution that an operating system can schedule for computation. Threads exist within processes and are the actual units of work. A single process can have multiple threads running concurrently, each executing a different task.

Simplified Understanding

A thread is an execution stream within a process, serving as the basic unit for CPU scheduling and dispatch. It's a more granular, independently runnable unit than a process.

A process can consist of several threads. Threads within the same process share most of the application's data structures and resources. However, processes have independent address spaces, meaning the failure of one process typically doesn't affect others (in protected mode). In contrast, threads lack separate address spaces; the termination of one thread can lead to the termination of the entire process. While multithreaded programs can be more efficient for concurrent operations sharing data, they are less robust than multi-process applications. Conversely, multi-process applications are more robust but incur higher resource costs during process switching.

Process: Refers to an application currently running in the system. It's the minimum unit for resource allocation.

Thread: The fundamental unit for CPU time allocation or an independent execution flow within a process. It's the minimum unit of program execution.

Concurrency vs. Parallelism

The key distinctions between concurrency and parallelism lie in the tasks handled, their existence, and CPU resource utilization.

Task Handling

  • Concurrency: Involves a single CPU processor handling multiple thread tasks. From a macroscopic view, tasks appear to run simultaneously. Microscopically, the CPU rapidly alternates between threads, dividing execution time into small slots allocated to each thread. While one thread runs, others are in a suspended state.
  • Parallelism: Involves multiple CPU processors simultaneously handling multiple thread tasks. When one CPU executes one thread, another CPU can execute a different thread independently, without competing for CPU resources.

Existence

  • Concurrency: Can occur in systems with a single CPU processor or multiple CPU processors. Even in a multi-processor system, a single CPU can still manage concurrent operations.
  • Parallelism: Exclusively occurs in systems with multiple CPU processors.

CPU Resource Utilization

  • Concurrency: Threads compete for CPU resources, using them in rotation. The CPU fairly distributes time slices among threads.
  • Parallelism: Threads do not compete for CPU resources, as each thread is handled by a separate CPU processor.

Serial Execution

Serial execution means tasks are performed sequentially. One task must complete before the next can begin. A single-threaded program executes serially.

Core Concepts of Concurrent Programming

  • Atomicity: Operations are indivisible; they either succeed entirely or fail entirely.
  • Visibility: Changes made to a shared variable by one thread are immediately visible to other threads.
  • Ordering: Program execution follows the sequence defined by the code. However, processors may reorder instructions for optimization.

Java Multithreading Implementation Methods

  1. Extending the Thread class.
  2. Implementing the Runnable interface.
  3. Using Callable and Future.
  4. Utilizing thread pools.

Thread States

Java defines thread states in the java.lang.Thread.State enumeration:


public enum State {
    NEW,         // Thread has been created but not yet started.
    RUNNABLE,    // Thread is ready to run or running.
    BLOCKED,     // Thread is waiting to acquire a lock.
    WAITING,     // Thread is waiting indefinitely for another thread.
    TIMED_WAITING, // Thread is waiting for a specified duration.
    TERMINATED   // Thread has completed its execution.
}

Detailed State Meanings:

  • NEW: The initial state of a thread that has been created but its start() method has not yet been called.
  • RUNNABLE: After start() is invoked, the thread enters this state. It signifies that the thread is eligible for execution and is waiting for the CPU schedulerr.
  • BLOCKED: Occurs when a thread attempts to acquire a lock that is held by another thread. The thread remains in this state until the lock is released and it can reacquire it.
  • WAITING: A thread enters this state when it calls Object.wait() or Thread.join(). It waits for a specific action from another thread (e.g., notification via notify()/notifyAll() or the termination of another thread).
  • TIMED_WAITING: A thread enters this state when it calls Thread.sleep(long), Object.wait(long), or Thread.join(long). It waits for a specified period.
  • TERMINATED: The final state of a thread after it has completed its execution.

1. Extending the Thread Class

  1. Create a subclass that extends Thread.
  2. Override the run() method to define the thread's task.
  3. Instantiate the subclass.
  4. Call the start() method on the object to begin execution.

Difference between start() and run()

  • start(): Initiates the thread's execution. It allocates a new execution thread and then invokes the overridden run() method within that new thread. A thread object's start() method can only be called once.
  • run(): If called directly, the run() method executes within the calling thread (typically the main thread), not in a separate thread.

public class TicketSeller extends Thread {
    private static int remainingTickets = 100;

    @Override
    public void run() {
        while (remainingTickets > 0) {
            sellTicket();
            try {
                Thread.sleep(100); // Simulate ticket selling time
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }
    }

    private synchronized void sellTicket() {
        if (remainingTickets > 0) {
            System.out.println(Thread.currentThread().getName() + " sold ticket #" + (101 - remainingTickets));
            remainingTickets--;
        }
    }

    public static void main(String[] args) {
        TicketSeller seller1 = new TicketSeller();
        TicketSeller seller2 = new TicketSeller();
        TicketSeller seller3 = new TicketSeller();

        seller1.start();
        seller2.start();
        seller3.start();
    }
}

2. Implementing the Runnable Interface

  1. Create a class that implements the Runnable interface.
  2. Implement the run() method, defining the task.
  3. Instantiate the implementing class.
  4. Create a Thread object, passing the Runnable instance to its constructor.
  5. Call the start() method on the Thread object.

public class TicketWindowDemo {
    public static void main(String[] args) {
        TicketCounter counter = new TicketCounter();

        Thread window1 = new Thread(counter, "Window 1");
        Thread window2 = new Thread(counter, "Window 2");
        Thread window3 = new Thread(counter, "Window 3");

        window1.start();
        window2.start();
        window3.start();
    }
}

class TicketCounter implements Runnable {
    private int ticketsAvailable = 100;

    @Override
    public void run() {
        while (ticketsAvailable > 0) {
            try {
                Thread.sleep(50); // Simulate ticket selling delay
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
            sellTicket();
        }
    }

    private synchronized void sellTicket() {
        if (ticketsAvailable > 0) {
            System.out.println(Thread.currentThread().getName() + " sold ticket #" + (101 - ticketsAvailable));
            ticketsAvailable--;
        }
    }
}

3. Implementing Callable and Future

Callable is more powerful than Runnable as its call() method can return a result, throw checked exceptions, and supports generic return types. It requires the use of FutureTask to retrieve the result.

  1. Create a class implementing the Callable interface.
  2. Implemnet the call() method, which contains the thread's logic and returns a result.
  3. Instantiate the Callable implementation.
  4. Create a FutureTask object, passing the Callable instance.
  5. Create a Thread object, passing the FutureTask to its constructor, and start the thread.
  6. Use the get() method of the FutureTask object to retrieve the result.

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class CallableDemo {
    public static void main(String[] args) {
        NumberCalculator calculator = new NumberCalculator();
        FutureTask<integer> futureTask = new FutureTask<>(calculator);

        Thread workerThread = new Thread(futureTask, "CalculatorThread");
        workerThread.start();

        try {
            // Get the result from the Callable
            Integer sumOfEvens = futureTask.get();
            System.out.println(Thread.currentThread().getName() + ": Sum of even numbers = " + sumOfEvens);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

class NumberCalculator implements Callable<integer> {
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 0; i <= 100; i += 2) {
            System.out.println(Thread.currentThread().getName() + " processing: " + i);
            sum += i;
            // Simulate some work
            Thread.sleep(10);
        }
        return sum;
    }
}
</integer></integer>

4. Using Thread Pools

Thread pools pre-create threads and manage them, allowing for reuse and reducing the overhead of frequent thread creation and destruction. This improves performance and resource management.

Thread Pool Design Principles

  • A task queue holds submitted tasks.
  • Multiple "worker" threads are created to process tasks from the queue.
  • When the task queue is empty, worker threads wait.
  • When a task is added, a waiting thread is notified, retrieves the task, executes it, and then returns to waiting.

Thread Pool Execution Flow

  1. Check for idle threads in the pool. If available, use one to execute the task.
  2. If no idle threads are available, create a "core" thread if the core pool size hasn't been reached.
  3. If core threads are all busy, add the task to a work queue.
  4. If the work queue is full, create a new thread (up to the maximum pool size).
  5. If both the maximum pool size and work queue are at capacity, a rejection policy is applied.

The sequence is: Core Threads -> Work Queue -> Maximum Threads -> Rejection Policy.

Thread Pool Rejection Policies

  • AbortPolicy: Throws a RejectedExecutionException when a task cannot be accepted.
  • CallerRunsPolicy: The thread that submitted the task executes it directly.
  • DiscardPolicy: The task is silently discarded.
  • DiscardOldestPolicy: The oldest task in the queue is discarded to make room for the new one.

Built-in Thread Pools (Executors utility class)

Java's java.util.concurrent.Executors provides factory methods for creating common thread pool configurations.

newCachedThreadPool()

Creates a flexible thread pool that creates new threads as needed but reuses existing ones if they are idle for 60 seconds.


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

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

        for (int i = 0; i < 5; i++) {
            final int taskId = i;
            executor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + " executing task " + taskId);
                try {
                    Thread.sleep(1000); // Simulate work
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }
        executor.shutdown(); // Initiates an orderly shutdown
    }
}

newFixedThreadPool(int nThreads)

Creates a thread pool with a fixed number of threads. Tasks are queued if all threads are busy.


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

public class FixedThreadPoolDemo {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3); // Pool with 3 threads

        ThreadPoolExecutor poolExecutor = (ThreadPoolExecutor) executor;
        System.out.println("Initial pool size: " + poolExecutor.getPoolSize());

        for (int i = 0; i < 7; i++) {
            final int taskId = i;
            executor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + " executing task " + taskId);
                try {
                    Thread.sleep(500); // Simulate work
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }
        System.out.println("Pool size after submitting tasks: " + poolExecutor.getPoolSize());
        executor.shutdown();
    }
}

Custom Thread Pool (ThreadPoolExecutor)

Provides fine-grained control over thread pool parameters.

ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)</runnable>

  • corePoolSize: The number of threads to keep in the pool even if they are idle.
  • maximumPoolSize: The maximum number of threads that can be created.
  • keepAliveTime: The maximum time that idle threads will wait for new tasks before terminating.
  • unit: The time unit for keepAliveTime.
  • workQueue: The queue used to hold tasks before they are executed.
  • threadFactory: Factory for creating new threads.
  • handler: The rejection policy to use when tasks cannot be executed.

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class CustomThreadPoolDemo {
    public static void main(String[] args) {
        // Core: 2 threads, Max: 4 threads, Queue: 5 tasks, Keep-alive: 60s
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2, // corePoolSize
                4, // maximumPoolSize
                60L, // keepAliveTime
                TimeUnit.SECONDS, // unit
                new ArrayBlockingQueue<>(5), // workQueue
                Executors.defaultThreadFactory(), // threadFactory
                new ThreadPoolExecutor.AbortPolicy() // handler
        );

        for (int i = 0; i < 10; i++) {
            final int taskId = i;
            executor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + " executing task " + taskId);
                try {
                    Thread.sleep(1000); // Simulate work
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }
        // Note: In a real application, you'd manage the executor lifecycle (shutdown, awaitTermination)
        executor.shutdown();
    }
}

Other Executors Factory Methods

  • newSingleThreadExecutor(): Creates an executor that uses a single worker thread.
  • newSingleThreadScheduledExecutor(): Creates a single-threaded executor that can schedule commands to run after a given delay or execute periodically.
  • newScheduledThreadPool(int corePoolSize): Creates a thread pool that can schedule commands.
  • newWorkStealingPool(): Creates a thread pool that uses the common ForkJoinPool, potentially improving performance for CPU-bound tasks.

Thread Deadlock

What is Deadlock?

Deadlock occurs when two or more threads are blocked forever, each waiting for the other to release a resource (lock) that it needs. This leads to a program halt.

Conditions for Deadlock (Coffman conditions):

  • Mutual Exclusion: At least one resource must be held in a non-sharable mode.
  • Hold and Wait: A thread holds at least one resource and is waiting to acquire additional resources held by other threads.
  • No Preemption: Resources cannot be forcibly taken from a thread holding them.
  • Circular Wait: A set of threads {T0, T1, ..., Tn} exists such that T0 is waiting for a resource held by T1, T1 is waiting for a resource held by T2, ..., Tn is waiting for a resource held by T0.

Java Locking Mechanisms

Concept

Locks are synchronization mechanisms used to control access to shared resources in multithreaded programming. They prevent multiple threads from modifying or reading shared data simultaneously, ensuring thread safety and coordinating access.

Lock Classifications

  • Optimistic Locking: Assumes that data conflicts are rare. It doesn't acquire locks initially. If a write operation detects a conflict (e.g., using version numbers or Compare-And-Swap (CAS)), it retries or handles the conflict.
  • Pessimistic Locking: Assumes conflicts are likely. It acquires locks on shared data before accessing it to prevent other threads from modifying it. Examples include synchronized blocks/methods and ReentrantLock.

Tags: java multithreading Concurrency parallelism thread states

Posted on Sat, 25 Jul 2026 16:42:49 +0000 by Lateuk