Java thread pool rejection policy

Posted by jwmessiah on Sat, 30 Oct 2021 16:22:55 +0200

0 pre dependency

In this example, assertj core will be used to complete the single test. maven depends on the following:

    <!-- https://mvnrepository.com/artifact/org.assertj/assertj-core -->
    <dependency>
      <groupId>org.assertj</groupId>
      <artifactId>assertj-core</artifactId>
      <version>3.21.0</version>
      <scope>test</scope>
    </dependency>

1 Abort Policy

The default policy, which throws a RejectedExecutionException exception when the task reaches the limit.

The interface is generally used for rapid response, which is suitable for use. It can find problems in time by quickly generating exceptions. Avoid the situation that the client call has timed out and the server is still processing the task.

        @Test
    public void testAbortPolicy() {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 0, MILLISECONDS,
            new SynchronousQueue<>(),
            new ThreadPoolExecutor.AbortPolicy());

        executor.execute(() -> waitFor(250));

        assertThatThrownBy(() -> executor.execute(() -> System.out.println("Will be rejected")))
            .isInstanceOf(RejectedExecutionException.class);
    }

Because the first task takes a long time, the thread pool will throw a RejectedExecutionException exception when submitting the second task

2 Caller-Runs Policy

Compared with other strategies, this strategy will execute the task by calling the thread when the task reaches the limit.

@Test
    public void testCallersRunPolicy() {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 0, MILLISECONDS,
            new SynchronousQueue<>(),
            new ThreadPoolExecutor.CallerRunsPolicy());

        executor.execute(() -> waitFor(25000));
        long startTime = System.currentTimeMillis();
        System.out.println("1 run");
        executor.execute(() -> waitFor(2000));
        long blockedDuration = System.currentTimeMillis() - startTime;
        System.out.println("2 run");
        executor.execute(() -> waitFor(2000));
        assertThat(blockedDuration).isGreaterThanOrEqualTo(500);
    }

After submitting the first task, the actuator cannot accept new tasks. Therefore, the calling thread will block until the task in the calling thread is completed.

Caller runs policy is a simple way to implement current limiting. Therefore, it can be considered in some consumer tasks, but it is not recommended to use the c-Oriented and high response interface to avoid the situation that the client call has timed out and the server is still processing the task.

3 Discard Policy

When the submission of a new task fails, the submitted new task will be discarded without throwing an exception.

@Test
public void testDiscardPolicy() throws Exception{
     ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 0, MILLISECONDS,
         new SynchronousQueue<>(),
         new ThreadPoolExecutor.DiscardPolicy());

     executor.execute(() -> waitFor(100));

     BlockingQueue<String> queue = new LinkedBlockingDeque<>();
     executor.execute(() -> queue.offer("Discarded Result"));
     assertThat(queue.poll(200, MILLISECONDS)).isNull();
 }

Exceptions are very important for finding problems, so you should carefully consider using this strategy to see whether exceptions can be ignored.

4 Discard-oldest

When the submitted task reaches the limit, remove the task in the queue header, and then resubmit the new task.

@Test
public void testDiscardOldestPolicy() {
    ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 0, MILLISECONDS,
        new ArrayBlockingQueue<>(2),
        new ThreadPoolExecutor.DiscardOldestPolicy());

    executor.execute(() -> waitFor(100));

    BlockingQueue<String> queue = new LinkedBlockingDeque<>();
    executor.execute(() -> queue.offer("First"));
    executor.execute(() -> queue.offer("Second"));
    executor.execute(() -> queue.offer("Third"));
    executor.execute(() -> queue.offer("Fourth"));
    waitFor(150);

    List<String> results = new ArrayList<>();
    queue.drainTo(results);

    assertThat(results).containsExactlyInAnyOrder("Third","Fourth");
}

The above test code uses a bounded blocking queue with a length of 2.

  • The first task monopolizes a thread for 100 milliseconds

  • Then the second and third tasks will be successfully added to the thread queue

  • When the 4th and 5th tasks arrive, delete the earliest submitted tasks in the queue according to the discard oldest policy, so as to make room for new tasks

Therefore, the final result is that the results array will contain two strings "Third" and "Fourth".

There are some problems when this strategy is used with priority queues. Because the priority queue header element has the highest priority. This strategy causes the most important tasks to be discarded.

5 custom policy

We can also implement a custom rejection policy by implementing the RejectedExecutionHandler interface.

class GrowPolicy implements RejectedExecutionHandler {

    private final Lock lock = new ReentrantLock();

    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        lock.lock();
        try {
            executor.setMaximumPoolSize(executor.getMaximumPoolSize() + 1);
        } finally {
            lock.unlock();
        }

        executor.submit(r);
    }

ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 0, MILLISECONDS, 
  new ArrayBlockingQueue<>(2), 
  new GrowPolicy());

executor.execute(() -> waitFor(100));

BlockingQueue<String> queue = new LinkedBlockingDeque<>();
executor.execute(() -> queue.offer("First"));
executor.execute(() -> queue.offer("Second"));
executor.execute(() -> queue.offer("Third"));
waitFor(150);

List<String> results = new ArrayList<>();
queue.drainTo(results);

assertThat(results).contains("First", "Second", "Third");

In this example, when the thread pool is saturated, we will increase the max pool size, and then resubmit the same task. With this strategy, after the task in the above example is rejected, the fourth task will be executed.

If the default policy cannot meet the requirements, it can be extended through the user-defined extension mechanism.

6. Shutdown triggers the rejection policy

These rejection policies are triggered not only in actuators that exceed the configuration limit, but also after the shutdown method is called.

ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 0, MILLISECONDS, new LinkedBlockingQueue<>());
executor.shutdownNow();

assertThatThrownBy(() -> executor.execute(() -> {}))
 .isInstanceOf(RejectedExecutionException.class);


ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 0, MILLISECONDS, new LinkedBlockingQueue<>());
executor.execute(() -> waitFor(100));
executor.shutdown();

assertThatThrownBy(() -> executor.execute(() -> {}))
  .isInstanceOf(RejectedExecutionException.class);

After the shutdown method is invoked in the example, the refusal policy is still triggered when the task is submitted again.

Topics: Java Back-end