Почему дочерний поток, использующий TreadPoolExecutor, не должен быть представлен какому-либо унаследованному контексту родительского потока?

Я сделал компонент, реализующий ApplicationListener<AuthenticationSuccessEvent> и должен регистрировать IP-адрес, IP-адрес можно получить из HttpServletRequest. По-видимому, этот компонент запускается в дочернем потоке, поэтому мне нужно было установить свойствоThreadContextInheritable верно на DispatcherServlet компонент, чтобы получить доступ к HttpServletRequest. (Я пробовал использовать RequestContextListener, но это не помогло).

В документации Spring, которую можно найти здесь, указано следующее предупреждение, когда вы устанавливаетеThreadContextInheritable к true.

ПРЕДУПРЕЖДЕНИЕ. Не используйте наследование для дочерних потоков, если вы обращаетесь к пулу потоков, который настроен на добавление новых потоков по запросу (например, JDK ThreadPoolExecutor), поскольку это приведет к раскрытию унаследованного контекста для такого объединенного потока.

У меня вопрос: почему раскрытие унаследованного контекста объединенному потоку может быть таким плохим? Что могло пойти не так в таком случае? Кроме того, означают ли они дочерний поток, который используетThreadPoolExecutor экземпляр ИЛИ означают ли они дочерний поток, созданный с использованием ThreadPoolExecutor?

0 ответов

InheritableThreadLocal расширяет ThreadLocal и используется, когда нам нужно автоматически передать значения локальных атрибутов родительского потока дочернему потоку при создании.

Это наследование отлично работает, когда вы каждый раз создаете новый дочерний поток и не используете повторно уже созданный поток.

Давайте рассмотрим сценарий - у нас есть пул потоков размером 5. Когда мы отправляем первые 5 задач в пул потоков для обработки для каждой задачи, создается новый поток, поэтому наследование локального атрибута потока работает отлично. Проблема начинается с 6-го запроса. Когда вы отправляете 6-ю задачу, новый поток не создается, но уже созданный поток из пула потоков повторно используется для его обработки. В 6-м запросе значения локальных атрибутов родительского потока не наследуются дочернему потоку, поскольку мы не создаем новый поток, а повторно используем уже созданный поток из пула.

Этот фрагмент кода объяснит суть -

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

public class DeadlyComboTest {

    public static void main(String[] args) throws InterruptedException {    
        int noOfThreads     = 5;
        //Execution 1
        int threadPoolSize  = noOfThreads;
        
        //Execution 2 : uncomment below line and comment line above
        //int threadPoolSize = noOfThreads/2;
            
        //1. create thread pool
        ExecutorService executor = Executors.newFixedThreadPool(threadPoolSize);  

        //2. create Inheritable thread local
        InheritableThreadLocal<Object> value = new InheritableThreadLocal<>(); 

        //3. create new command and execute using thread pool created in step 1
        for (int i = 1; i <= noOfThreads; i++) {
            value.set(i);
            executor.execute(() -> printThreadLocalValue(value));
        }   
        executor.shutdown();
    }
   
    private static void printThreadLocalValue(ThreadLocal<Object> value) {
        System.out.println(Thread.currentThread().getName() + " = " + value.get());
    }
}
Выполнение 1: noOfThreads = threadPoolSize = 5
    OUTPUT: you may get the output in different sequence
        pool-1-thread-1 = 1
        pool-1-thread-2 = 2
        pool-1-thread-3 = 3
        pool-1-thread-4 = 4
        pool-1-thread-5 = 5
        

Все выглядит хорошо. Поскольку при каждом запросе создается новый поток, а дочерний поток правильно наследует локальные значения родительского потока.

Выполнение 2: noOfThreads = 5 && threadPoolSize = noOfThreads/2 = 2
  OUTPUT:
        pool-1-thread-1 = 1
        pool-1-thread-2 = 2
        pool-1-thread-1 = 1
        pool-1-thread-2 = 2
        pool-1-thread-1 = 1


        

Только для первых двух запросов наследование локального потока работает правильно, поскольку размер пула потоков равен 2, поэтому для первых двух запросов создаются и объединяются новые потоки. При третьем запросе повторно используются уже созданные потоки из пула, которые все еще имеют унаследованные значения старого потока.

Выполнение 2 - это реальный сценарий, в котором потоки из пула будут повторно использоваться.

Что может пойти не так, если мы используем комбинацию пула потоков и InheritableThreadLocal?

Это вызовет непреднамеренную утечку информации, поскольку рабочий / дочерний поток из пула будет передавать значения локальных атрибутов потока одного потока другому.

Если ваша бизнес-логика зависит от значений этих атрибутов, вам будет сложно отслеживать ошибки.

Вот почему документация Spring предостерегает от использования этой комбинации пула потоков и InheritableThreadLocal.

Другие вопросы по тегам