Implicit coupling between tasks and execution strategies

We already know that the Executor framework can decouple task submission from task execution strategies. Like many decoupled operations on complex processes, this claim is somewhat overblown. Although the Executor framework provides considerable flexibility for both formulating and modifying execution strategies, not all tasks are suitable for all execution strategies. Some types of tasks require an explicit execution policy, including:

dependent tasks. Most well-behaved tasks are independent: they do not depend on the execution timing, execution results, or other effects of other tasks. When executing independent tasks in the thread pool, the size and configuration of the thread pool can be changed at will, and these modifications will only affect the execution performance. However, if tasks submitted to the thread pool depend on other tasks, this implicitly imposes constraints on execution policies that must be carefully maintained to avoid liveness problems.

Tasks that use the thread confinement mechanism. A single-threaded Executor can make a stronger commitment to concurrency than a thread pool. They ensure that tasks do not execute concurrently, allowing you to relax your code's requirements for thread safety. Objects can be enclosed in a task thread so that tasks executing in that thread do not need to synchronize when accessing the object, even if these resources are not thread-safe. This situation will form an implicit coupling between the task and the execution strategy - the task requires the Executor where it executes to be single-threaded. If the Executor is changed from a single-threaded environment to a thread pool environment, thread safety will be lost.

Tasks that are sensitive to response time. GUI applications are sensitive to response time: users will be dissatisfied if there is a long delay before they get visible feedback after clicking a button. Submitting a long-running task to a single-threaded Executor, or submitting multiple long-running tasks to a thread pool with only a small number of threads, will degrade the responsiveness of services managed by the Executor sex.

                  

This requirement does not need to be so strict, just ensure that tasks will not execute concurrently, and provide sufficient synchronization mechanisms so that the effect of one task on memory must be visible to the next task-this is the guarantee provided by newSingleThreadExecutor.

Tasks using ThreadLocal. ThreadLocal enables each thread to have a private "version" of a variable. However, Executors are free to reuse these threads whenever conditions permit. In the standard Executor implementation, idle threads are reclaimed when execution demand is low, new threads are added when demand increases, and a new worker is started if an unchecked exception is thrown from a task thread to replace the thread that threw the exception. Using ThreadLocal in a thread pool thread only makes sense if the lifetime of the thread-local value is bounded by the lifetime of the task, and you should not use ThreadLocal in a thread pool thread to pass values ​​between tasks.

Thread pool performance is best when tasks are all of the same type and independent of each other. If you mix long-running tasks with short-running tasks, unless the thread pool is very large, it will likely cause "congestion". If the submitted task depends on other tasks, then unless the thread pool is infinite, it may cause deadlock. Fortunately, in typical web-based server applications—web servers, mail servers, file servers, etc.—the requests are usually homogeneous and independent of each other.

In some tasks, it is necessary to have or exclude a certain execution policy. If some tasks depend on other tasks, the thread pool will be required to be large enough to ensure that their dependent tasks will not be placed in the waiting queue or rejected, while tasks using the thread closure mechanism need to be executed serially. By documenting these requirements, future maintainers of the code cannot compromise safety or liveness by using an inappropriate execution strategy.

  thread starvation deadlock

In a thread pool, if tasks depend on other tasks, deadlocks can occur. In a single-threaded Executor, if a task submits another task to the same Executor and waits for the result of the submitted task, then a deadlock is usually caused. The second task stays in the work queue and waits for the first task to complete, but the first task cannot complete because it is waiting for the second task to complete. In larger thread pools, the same problem can occur if all threads executing tasks are blocked waiting for other tasks still in the work queue. This phenomenon is called thread starvation deadlock (Thread Starvation Deadlock), as long as the tasks in the thread pool need to wait indefinitely for some resources or conditions that must be provided by other tasks in the pool, such as a task waiting for another task Return value or execution result, then unless the thread pool is large enough, thread starvation deadlock will occur.

An example of a thread starvation deadlock is given in ThreadDeadlock in Listing 8-1. RenderPageTask submits two tasks to the Executor to get the header and footer of the web page, draw the page, wait for the results of the task of getting the header and footer, and then combine the header, page body and footer to form the final page. If you use a single-threaded Executor, ThreadDeadlock will often deadlock. Similarly, if the thread pool is not large enough, when multiple tasks coordinate with each other through the Barrier mechanism, thread starvation deadlock will result.

Future<String>header, footer;

header =exec. submit(new LoadFileTask("header. html"));

footer =exec. submit(new LoadFileTask("footer. html"));

String page =renderBody();

// Deadlock will occur - because the task is waiting for the result of the subtask

return header. get()+page +footer. get();

Whenever a dependent Executor task is submitted, it is clear that thread "starvation" deadlock may occur, so it is necessary to record the size limit or configuration limit of the thread pool in the code or configuration file of the Executor.

In addition to explicit limits on thread pool size, there may be some implicit limits due to constraints on other resources. If the application uses a JDBC connection pool of 10 connections, and each task needs a database connection, then the thread pool will appear to have only 10 threads, because when more than 10 tasks are exceeded, new tasks need to wait for other tasks to release connections .

  long running tasks

If tasks are blocked for too long, the responsiveness of the thread pool will become poor even if there is no deadlock. Tasks with long execution times will not only block the thread pool, but even increase the service time of tasks with shorter execution times. If the number of threads in the thread pool is much smaller than the number of long-running tasks in steady state, then all threads may end up running these long-running tasks, affecting overall responsiveness.

One technique to mitigate the effects of long-running tasks is to limit the amount of time a task waits for a resource, rather than waiting indefinitely. In most of the blockable methods of the platform class library, both time-limited and unlimited versions are defined, such as Thread.join, BlockingQueue.put, CountDownLatch.await, and Selector.select. If the wait times out, the task can be marked as failed and either aborted or put back into the queue for subsequent execution. In this way, regardless of whether the final result of the task is successful, this method can ensure that the task can always continue to execute, and release the thread to perform some tasks that can be completed faster. If the thread pool is always full of blocked tasks, it may also indicate that the thread pool size is too small.

Guess you like

Origin blog.csdn.net/2301_78145678/article/details/131031059
Recommended