Java Concurrency Programming Guide (4): Thread Executor

1. Create a thread executor:

The first step in using the Executor framework is to create an object of the ThreadPoolExecutor class. You can use the four constructors provided by this class or the Executors factory class to create a ThreadPoolExecutor. With an executor, you can submit Runnable or Callable objects to the executor for execution.

An example that simulates a web server:

// 1. First, implement a task that can be executed by the server. Create a Task class that implements the Runnable interface.
class Task implements Runnable {
    //2. Declare a property of type Date named initDate to store the task creation date, and a property of type String named name to store the name of the task.
    private Date initDate;
    private String name;

    // 3. Implement the Task constructor and initialize these two properties.
    public Task(String name) {
        initDate = new Date();
        this.name = name;
    }

    // 4. Implement the run() method.
    @Override
    public void run() {
        // 5. First, write the initDate property and the actual date (which is the start date of the task) to the console.
        System.out.printf("%s: Task %s: Created on: %s\n", Thread.currentThread().getName(), name, initDate);
        System.out.printf("%s: Task %s: Started on: %s\n", Thread.currentThread().getName(), name, new Date());
        // 6. Then, make the task sleep for a random amount of time.
        try {
            Long duration = (long) (Math.random() * 10);
            System.out.printf("%s: Task %s: Doing a task during %dseconds\n", Thread.currentThread().getName(), name, duration);
            TimeUnit.SECONDS.sleep(duration);
        } catch (InterruptedException e) {
            e.printStackTrace ();
        }
        //7. Finally, write the task completion time to the console.
        System.out.printf("%s: Task %s: Finished on: %s\n", Thread.currentThread().getName(), name, new Date());
    }
}
// 8. Now, implement the server class to perform all the tasks accepted by the executor. Create a Server class.
class Server {
    // 9. Declare a property of type ThreadPoolExecutor named executor.
    private ThreadPoolExecutor executor;
    // 10. Implement the Server constructor and use the Executors class to initialize the ThreadPoolExecutor object.
    public Server(){
        executor=(ThreadPoolExecutor)Executors.newCachedThreadPool();
    }
    // 11. Implement the executeTask() method, receive the Task object as a parameter and submit it to the executor. First, write a message to the console that a new task has arrived.
    public void executeTask(Task task) {
        System.out.printf("Server: A new task has arrived\n");
        //12. Then, call the execute() method of the executor to submit the task.
        executor.execute(task);
        //13. Finally, write the data of the executors to the console to see their status.
        System.out.printf("Server: Pool Size: %d\n", executor.getPoolSize());
        System.out.printf("Server: Active Count: %d\n", executor.getActiveCount());
        System.out.printf("Server: Completed Tasks: %d\n", executor.getCompletedTaskCount());
    }
    //14. Implement the endServer() method. In this method, call the executor's shutdown() method to end the task execution.
    public void endServer() {
        executor.shutdown();
    }
}
// 15. Finally, implement the main class for this example, create the Main class, and implement the main() method.
class Main3 {
    public static void main(String[] args) {
        Server server=new Server();
        for (int i=0; i<100; i++){
            Task task=new Task("Task "+i);
            server.executeTask(task);
        }
        server.endServer();
    }
}
The Server class is the key to this example. It creates and executes tasks using ThreadPoolExecutor.
The first important point is to create a ThreadPoolExecutor in the constructor of the Server class. ThreadPoolExecutor has 4 different constructors, but due to their complexity, Java Concurrency API provides Executors class to construct executors and other related objects. Even though we can create a ThreadPoolExecutor through any of the constructors of the ThreadPoolExecutor class, it is recommended to use the Executors class.
In this example, you have created a cache thread pool using the newCachedThreadPool() method. This method returns an ExecutorService object, so it is cast to the ThreadPoolExecutor type to access all its methods. The cache thread pool you have created will create new threads when new tasks need to be performed. If they have completed running tasks and become available, these threads will be reused. The benefit of thread reuse is that it reduces thread creation time. The downside of a cached thread pool is that threads are constantly being created for new tasks, so if you submit too many tasks to executors, you can overload the system.
Note: Use executors created by the newCachedThreadPool() method only if you have a reasonable number of threads or the task has a short execution time.
Once you create an executor, you can submit tasks of type Runnable or Callable using the execute() method. In this example, you submit an object of the Task class that implements the Runnable interface.
You also print some log information about executor information. In particular, you can use the following methods:
  • getPoolSize(): This method returns the actual number of threads in the thread pool.
  • getActiveCount(): This method returns the number of threads that are executing tasks in the executor.
  • getCompletedTaskCount(): This method returns the number of tasks completed by the executor.

A key aspect of the ThreadPoolExecutor class and executors in general is that you must explicitly end it. If you do not do this, the executor will continue its execution and the program will not end. If the executor has no tasks to execute, it continues to wait for new tasks and does not end its execution. A Java application will not end until all non-daemon threads complete their execution. So, if you don't end this executor, your application won't end.

When the executor has completed all pending tasks, you can use the shutdown() method of the ThreadPoolExecutor class to indicate that you want to terminate the executor. After you call the shutdown() method, if you try to submit another task to the executor, it will reject it and throw a RejectedExecutionException.

The ThreadPoolExecutor class provides many methods to get its status. In this example, we use the getPoolSize(), getActiveCount() and getCompletedTaskCount() methods to get the pool size, number of threads, and number of completed tasks for the executor. You can also use the getLargestPoolSize() method, which returns the maximum number of threads in the pool at a time.
The ThreadPoolExecutor class also provides other methods related to ending the executor, these methods are:

  • shutdownNow(): This method shuts down the executor immediately. It doesn't execute pending tasks, but it returns a list of pending tasks. When you call this method, running tasks continue their execution, but this method does not wait for them to finish.
  • isTerminated(): This method returns true if you have called the shutdown() or shutdownNow() method and the executor has finished processing to close it.
  • isShutdown(): This method returns true if you call the shutdown() method in the executor.
  • awaitTermination(long timeout, TimeUnit unit): This method blocks the calling thread until the executor's task ends or times out.


2. Create a fixed-size thread executor:

When you use the basic ThreadPoolExecutor created by the newCachedThreadPool() method of the Executors class, you have the problem of the number of threads the executor is running at a given moment. This executor creates a thread for each received task (if there are no idle threads in the pool), so if you submit a lot of tasks, and they have a long (execution) time, you will overload the system and throw Problems with poor application performance.

If you want to avoid this problem, the Executors class provides a method newFixedThreadPool() to create fixed-size thread executors. This executor has a maximum number of threads. If you submit tasks that exceed this maximum number of threads, the executor will not create additional threads, and the remaining tasks will block until the executor has idle threads. This behavior ensures that the executor will not cause problems with poor application performance.


3. The executor executes the task that returns the result:

An advantage of the Executor framework is that you can execute tasks that return results concurrently. The Java Concurrency API is implemented using the following two interfaces:

  • Callable : This interface has a call() method. In this method, you have to implement the (processing) logic of the task. The Callable interface is a parameterized interface. Means you have to indicate the data type returned by the call() method.
  • Future : This interface has methods to guarantee the fetching of the result of the Callable object and manage its state.
Future<Integer> result = executor.submit(calculator);


4. Run multiple tasks and process the first result:

A common problem in concurrent programming is that when there are multiple concurrent tasks to solve a problem, you are only interested in the first result of those tasks. For example, you want to sort an array. You have multiple sorting algorithms. You can enable them all, and get the first result (the result of the fastest algorithm for sorting the given array).
String result = executor.invokeAny(taskList);

5. Run multiple tasks and process all results:

The ThreadPoolExecutor class provides a method that allows you to submit a list of tasks to an executor and wait for all tasks to complete on this list.

List<Future<Result>> resultList = executor.invokeAll(taskList);

6. The executor delays running a task:

The executor framework provides the ThreadPoolExecutor class, which uses the threads in the pool to execute Callable and Runnable tasks, which avoids all thread creation operations. When you submit a task to an executor, it will be executed as soon as possible according to the executor's configuration. In some use cases, when you are not interested in performing tasks as quickly as possible. You may want to perform tasks after a period of time or periodically. For these purposes, the executor framework provides the ScheduledThreadPoolExecutor class.
schedule(Callable<V> callable, long delay, TimeUnit unit)

7. The executor periodically runs a task:

scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)

8. The executor cancels a task:

When you work with executors, you have to manage threads. You only implement Runnable or Callable tasks and submit them to executors. The executor is responsible for creating threads, managing them in the thread pool, and ending them when they are not needed. Sometimes, you want to cancel a task that has been submitted to the executor. In this case, you can use Future's cancel() method, which allows you to cancel.


9. The executor controls the completion of a task:

The FutureTask class provides a done() method that allows you to execute some code after the executor has finished executing the task. You can use it to do some post-processing, generate a report, send results via e-mail, or free up some resources. When the executed task is controlled by FutureTask, FutureTask will call this method internally. This method is called after the result of the task has been set and its state has changed to the isDone state, regardless of whether the task has been canceled or completed normally.

10. The executor separates the initiation of the task and the processing of the result:

Typically, when you use an executor to execute concurrent tasks, you will submit a Runnable or Callable task to the executor and get a Future object to control this method. You can find this situation where you need to submit the task to the executor in one object and the result of the processing in another object. Based on this situation, the Java Concurrency API provides the CompletionService class.

The CompletionService class has one method to submit the task to the executor and another method to get the Future object for the next task that has completed execution. Internally, it uses an Executor object to execute tasks. The advantage of this behavior is to share a CompletionService object and submit the task to the executor so other (objects) can process the result. The limitation is that the second object can only get Future objects that have completed the task of their execution, so these Future objects can only get the result of the task.

11. Executor controls rejected tasks:

When you want to end the execution of an executor, you use the shutdown() method to signal its end. The executor waits for the completion of tasks that are running or waiting for its execution, and then ends their execution.
If you submit a task to the executor between the shutdown() method and the end of the executor, the task will be rejected because the executor is no longer accepting new tasks. The ThreadPoolExecutor class provides a mechanism to not accept new tasks after calling shutdown(). Rejected tasks are managed in the executor
by implementing RejectedExecutionHandler .



Reference: "Java 7 Concurrency Cookbook"

                 《Java 9 Concurrency Cookbook Second Edition》


Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325948032&siteId=291194637