Java ThreadPoolExecutor afterExecute() Method

The ThreadPoolExecutor class in Java provides the afterExecute() method, which can be overridden to perform custom actions after the execution of each task. This guide will cover the usage of the afterExecute() method, explain how it works, and provide concise examples to demonstrate its functionality in real-world use cases.

Introduction

The afterExecute() method is a protected method of the ThreadPoolExecutor class that can be overridden to define custom behavior that should occur after the execution of each task. This can be useful for logging, cleanup, error handling, or other post-processing activities.

afterExecute Method Syntax

The syntax for the afterExecute method is as follows:

protected void afterExecute(Runnable r, Throwable t)
  • The method takes two parameters:
    • r of type Runnable, which represents the task that has completed.
    • t of type Throwable, which is the exception that caused the termination, or null if the execution completed normally.

Examples

Example 1: Logging Task Completion

In this example, we override the afterExecute() method to log the completion of each task.

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

public class LoggingThreadPoolExecutor extends ThreadPoolExecutor {

    public LoggingThreadPoolExecutor() {
        super(2, 4, 10, TimeUnit.SECONDS, Executors.newFixedThreadPool(4).getQueue());
    }

    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        System.out.println("Task completed: " + r.toString());
        if (t != null) {
            System.err.println("Task encountered an error: " + t.getMessage());
        }
    }

    public static void main(String[] args) {
        LoggingThreadPoolExecutor executor = new LoggingThreadPoolExecutor();

        for (int i = 0; i < 5; i++) {
            final int taskNumber = i + 1;
            executor.submit(() -> {
                System.out.println("Executing task " + taskNumber + " by " + Thread.currentThread().getName());
                try {
                    Thread.sleep(1000); // Simulate task execution
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        executor.shutdown();
    }
}

Output:

Executing task 1 by pool-1-thread-1
Executing task 2 by pool-1-thread-2
Executing task 3 by pool-1-thread-1
Executing task 4 by pool-1-thread-2
Executing task 5 by pool-1-thread-1
Task completed: java.util.concurrent.FutureTask@<hashcode>
Task completed: java.util.concurrent.FutureTask@<hashcode>
Task completed: java.util.concurrent.FutureTask@<hashcode>
Task completed: java.util.concurrent.FutureTask@<hashcode>
Task completed: java.util.concurrent.FutureTask@<hashcode>

Example 2: Handling Task Exceptions

In this example, we override the afterExecute() method to log any exceptions thrown by the tasks.

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

public class ExceptionHandlingThreadPoolExecutor extends ThreadPoolExecutor {

    public ExceptionHandlingThreadPoolExecutor() {
        super(2, 4, 10, TimeUnit.SECONDS, Executors.newFixedThreadPool(4).getQueue());
    }

    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        if (t != null) {
            System.err.println("Task encountered an error: " + t.getMessage());
        } else {
            System.out.println("Task completed successfully: " + r.toString());
        }
    }

    public static void main(String[] args) {
        ExceptionHandlingThreadPoolExecutor executor = new ExceptionHandlingThreadPoolExecutor();

        for (int i = 0; i < 5; i++) {
            final int taskNumber = i + 1;
            executor.submit(() -> {
                System.out.println("Executing task " + taskNumber + " by " + Thread.currentThread().getName());
                if (taskNumber == 3) {
                    throw new RuntimeException("Exception in task " + taskNumber);
                }
                try {
                    Thread.sleep(1000); // Simulate task execution
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        executor.shutdown();
    }
}

Output:

Executing task 1 by pool-1-thread-1
Executing task 2 by pool-1-thread-2
Executing task 3 by pool-1-thread-1
Executing task 4 by pool-1-thread-2
Executing task 5 by pool-1-thread-1
Task completed successfully: java.util.concurrent.FutureTask@<hashcode>
Task completed successfully: java.util.concurrent.FutureTask@<hashcode>
Task encountered an error: Exception in task 3
Task completed successfully: java.util.concurrent.FutureTask@<hashcode>
Task completed successfully: java.util.concurrent.FutureTask@<hashcode>

Example 3: Custom Task Completion Logic

In this example, we override the afterExecute() method to implement custom logic after the completion of each task.

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

public class CustomLogicThreadPoolExecutor extends ThreadPoolExecutor {

    public CustomLogicThreadPoolExecutor() {
        super(2, 4, 10, TimeUnit.SECONDS, Executors.newFixedThreadPool(4).getQueue());
    }

    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        if (t == null) {
            System.out.println("Task completed: " + r.toString());
            // Custom logic after task completion
            System.out.println("Performing custom logic after task completion.");
        } else {
            System.err.println("Task encountered an error: " + t.getMessage());
        }
    }

    public static void main(String[] args) {
        CustomLogicThreadPoolExecutor executor = new CustomLogicThreadPoolExecutor();

        for (int i = 0; i < 5; i++) {
            final int taskNumber = i + 1;
            executor.submit(() -> {
                System.out.println("Executing task " + taskNumber + " by " + Thread.currentThread().getName());
                try {
                    Thread.sleep(1000); // Simulate task execution
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        executor.shutdown();
    }
}

Output:

Executing task 1 by pool-1-thread-1
Executing task 2 by pool-1-thread-2
Executing task 3 by pool-1-thread-1
Executing task 4 by pool-1-thread-2
Executing task 5 by pool-1-thread-1
Task completed: java.util.concurrent.FutureTask@<hashcode>
Performing custom logic after task completion.
Task completed: java.util.concurrent.FutureTask@<hashcode>
Performing custom logic after task completion.
Task completed: java.util.concurrent.FutureTask@<hashcode>
Performing custom logic after task completion.
Task completed: java.util.concurrent.FutureTask@<hashcode>
Performing custom logic after task completion.
Task completed: java.util.concurrent.FutureTask@<hashcode>
Performing custom logic after task completion.

Conclusion

The ThreadPoolExecutor.afterExecute() method in Java provides a way to define custom behavior after the execution of each task. By overriding this method, you can implement logging, error handling, custom logic, or any other post-processing activities.

Comments