Ostatnio miałem przyjemność pracować nad funkcjonalnością, do której implementacji wykorzystywałem zmienne typu ThreadLocal. Dzięki temu miałem sposobność jeszcze raz przyjrzeć się koncepcji ThreadLocal. Idea tego podejścia jest całkiem prosta, każdy wątek ma własną instancję mapy ThreadLocalMap, w której przechowuje wartości zmiennych typu ThreadLocal. W momencie, kiedy chcesz uzyskać dostęp do wartości zmiennej przechowywanej ThreadLocal, twoja prośba delegowana jest do mapy ThreadLocalMap aktualnego wątku.

Kiedy chciałbyś zrobić zmienną typu ThreadLocal z dziedziczeniem, wystarczy, że wykorzystasz implementację InheritableThreadLocal. Implementacja ta zapewnia, że w momencie tworzenia nowego wątku otrzyma on wartości ze swojego rodzica.

Kiedy rozpoczynałem implementację wykorzystującą ThreadLocal, zacząłem się zastanawiać jakie mogą być potencjalne problemy. Na pierwszy rzut oka wszystko wygląda elegancko, ale wtedy pomyślałem o ExecutorService.

Code:


ExecutorService executor = Executors.newSingleThreadExecutor();

InheritableThreadLocal<Integer> value = new InheritableThreadLocal<>();
value.set(1);

print(value); // main=1
executor.execute(()->print(value)); // pool-thread-1=1

value.set(2);
print(value); // main=2
executor.execute(() -> print(value)); // pool-thread-1=1 !?!

private static void print(ThreadLocal<Integer> value) {
System.out.println(Thread.currentThread().getName() + "=" + value.get());
}

Console output:

main=1

main=2

pool-1-thread-1=1

pool-1-thread-1=1

Co się stało? Odpowiedzi są całkiem proste:

  • Wątek wykorzystywany przez ExecutorService został zainicjowany podczas pierwszego wywołania metody execute.
  • Kolejne wywołanie metody execute zostało wykonane z użyciem wątku, który powstał w czasie pierwszego uruchomienia.

Szybkie spojrzenie do implementacji ExecutorService potwierdza, że będzie to działało w taki sposób. ExecutorService nie „bawi się” zmiennymi typu ThreadLocal. Zacząłem się zastanawiać nad szybkim rozwiązaniem dla ExecutorService. Pomyślałem o Spring Framework i ich SecurityContext. Oni pewnie mieli te same problem przy okazji np. obsługi adnotacji @Async. Byłem przekonany, że mają jakieś rozwiązania tego problemu. Po chwili udało mi się odnaleźć DelegatingSecurityContextExecutor. W tej klasie znajdziesz inspiracje, jak można zbudować własny ExecutorServices lub wzbogacić Runnable tak, aby działało z ThreadLocal.