ThreadLocal
jest trochę jak świnka morska…
Słowem wstępu
Bohaterem tego wpisu jest java.lang.ThreadLocal
. Jak sama nazwa wskazuje klasa umożliwia trzymanie pewnej zmiennej w kontekście jednego wątku. Taką klasę można wykorzystać w różnych sytuacjach, a najbardziej typową jest tworzenie obiektów, które nie są thread-safe i przechowywanie takich obiektów osobno dla każdego wątku. Wówczas pozbywamy się wymaganej kosztownej synchronizacji.
Kanonicznym przykładem jest klasa SimpleDateFormatter
, która nie jest thread-safe.
Istnieje jeszcze inna klasa zastosowań ThreadLocal
. Polega ona na inicjalizacji na początku przetwarzania, następnie w czasie przetwarzania na pobraniu danej wartości (bądź – co gorsza – modyfikacji) a na końcu przetwarzania na usunięciu tej wartości. Przykładowo – Filtr Servletowy:
public class UserNameFilter implements Filter { public static final ThreadLocal USER_NAME = new ThreadLocal(); @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { try { USER_NAME.set("Dobromir"); filterChain.doFilter(servletRequest, servletResponse); } finally { USER_NAME.remove(); } } }
Takie zastosowanie to tak na prawdę taki lokalny dla wątku singleton. Taki mechanizm obrazuje mniej więcej:
Asdf movie
Kto takiego kodu nie popełnił, niech pierwszy rzuci kamień 😉
Czasem po prostu nie ma innej opcji, by przekazać coś z jednego miejsca w drugie, bo przykładowo ogranicza nas interfejs/zewnętrzna biblioteka. Jednak, gdy mamy możliwość przekazania czegoś w parametrze metody zamiast w ThreadLocal
, warto z tej możliwości skorzystać. Nawet, gdy to będzie przepchanie przez 20 ramek głębiej w stacktrace’ie.
Escape analysis
Warto kontekst przekazywać w parametrach z wielu powodów. Najważniejszym jest jawne ukazanie zależności potem, testowalność itp. Ale gdzieś na sam końcu jest też wydajność.
Dla każdej metody skompilowanej C2
jest uruchamiane Escape Analysis, która pozwala na unikanie fizycznego tworzenia obiektów. Jeśli jednak taki obiekt jest udostępniony w jakimś polu, to automatycznie uniemożliwiamy ominięcie tworzenia obiektu.
Implementacja ThreadLocal
Najprostsza implementacja tej idei to zwykła mapa HashMap
, która w kluczu przyjmuje Thread.getId()
. To rozwiązanie jest jednak zasadniczą wadę – jeśli wątek by zakończył swoje działanie, a wpis nie zostałby usunięty, wówczas mielibyśmy klasyczny przykład wycieku pamięci w Javie. Trzymanie jakiegoś rodzaju uchwytu do tych wpisów dla ThreadLocal
może i rozwiązało problem, ale mogłoby być kosztowne pamięciowo.
Dlatego OpenJDK robi to inaczej. W każdym obiekcie java.lang.Thread
istnieje pole threadLocals
będące instancją klasy ThreadLocal.ThreadLocalMap
. W tym polu przetrzymywane są wartości dla wszystkich ThreadLocal
. Jest to mapa, którą można określić jako HashMap
.
Gdy wołamy o ThreadLocal.get()
wywoływany jest następujący kawałek kodu:
public T get() { Thread t = Thread.currentThread(); ThreadLocal.ThreadLocalMap map = this.getMap(t); if (map != null) { ThreadLocal.ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { T result = e.value; return result; } } return this.setInitialValue(); } ThreadLocal.ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
Ta nieco zawiła implementacja trzyma wszystkie zmienne specyficzne dla wątku blisko reprezentacji tego wątku w Javie. Dzięki temu w czasie kończenia działania wątku, łatwo je udostępnić dla gc’ka (this.threadLocals = null
).
Czy ThreadLocal
ma jakieś super moce?
Pojęcie Thread-local storage jest pojęciem znanym i powszechnym w różnych językach programowania. Ponadto jest tak często nazywana pewna część pamięci natywnej wyłączna dla wątku systemu operacyjnego. Jednak w przypadku OpenJDK taka pamięć jest wykorzystywana co najwyżej przy jakichś metadanych GCka (wystarczy wyszukać w kodzie źródłowym OpenJDK terminu ThreadLocalStorage
). Całość implementacji ThreadLocal
bazuje na Heapie.
Co więcej, okazuje się, że ten ThreadLocal nie jest aż tak przywiązany do samego wątku, gdyż można go z poziomu innego wątku zmienić. Można to łatwo sprawdzić wykonując refleksyjną magię:
public class ThreadLocalExperiment { private static boolean work = true; private static final ThreadLocal THREAD_LOCAL = new ThreadLocal(); public static void main(String[] args) throws Exception { var thread1 = new Thread(() -> { THREAD_LOCAL.set(12); while (work) { } System.out.println(THREAD_LOCAL.get()); }); thread1.start(); var clazz = Thread.class; var field = clazz.getDeclaredField("threadLocals"); field.setAccessible(true); var threadLocals = field.get(thread1); var method = threadLocals.getClass().getDeclaredMethod("set", ThreadLocal.class, Object.class); method.setAccessible(true); method.invoke(threadLocals, THREAD_LOCAL, 24); work = false; } }
ThreadLocal
vs local variable
Generalnie warto też porównać, jaka jest różnica w wydajności między zmiennymi lokalnymi, a ThreadLocal
. Prosty benchmark ukazujący skalę różnicy wydajności:
@Benchmark @CompilerControl(CompilerControl.Mode.PRINT) public Integer local() { Integer i = 1; return i; } @Benchmark @CompilerControl(CompilerControl.Mode.PRINT) public Integer threadLocal() { THREAD_LOCAL.set(1); Integer i = THREAD_LOCAL.get(); THREAD_LOCAL.remove(); return i; }
Wyniki to 4,358 ± 0,039 ns/op dla zmiennej lokalnej oraz 41,359 ± 2,797 ns/op dla ThreadLocal
(1 ns to jedna milionowa milisekundy zatem niewiele 😉 ). Jednak samo sięganie na stertę zamiast na stos wątku jest już pewnym minusem. Ponadto te różnice w pewien sposób zależą od GC, którym wartości ThreadLocal
podlegają.
JIT
owi również nie jest łatwo zinterpretować wartości ThreadLocal
jako niezmienialne przez inne wątki. Chociaż swoją drogą mogą być zmienione jak wcześniej zostało wykazane. Brak możliwości zastosowania Escape Analysis również nie pomaga…
Ale o co chodzi z tą świnką morską?
ThreadLocal
to taka świnka morska, bo ani świnka, ani morska…
Ani nie są tą jakoś szczególnie wyłączne dane wątku, gdyż są na Heap
ie, współdzielone, a takie stricte dane wątku są na Off-Heap
ie. Ani też nie są to szczególnie lokalne dane – przeważnie to są singletony w kontekście wątku.
Niby blisko tego wątku to jest, ale jednak nie za bardzo…