Jeśli nie ThreadLocal to co? ScopedValue!

Co to jest ScopedValue?
ScopedValue to wartość, która może być widoczna i używana w metodach, a która nie została do nich przekazana jako ich parametr. Dotychczas taką rolę w języku Java pełniły zmienne ThreadLocal. Przyjrzyjmy się im odrobinę, żeby odświeżyć swoją wiedzę, a później popatrzymy, co nowego wnoszą ich nowoczesne zastępniki, czyli zmienne ScopedValue.
Co to jest zmienna ThreadLocal?
Zmienne ThreadLocal są deklarowane zazwyczaj jako prywante zmienne globalne (pola statyczne) i dają gwarancję, że wartość, którą z nich odczytujemy (metodą get()) to ta sama wartość, która została w nich zapisana (metodą set()) w tym samym wątku (lub ta, którą zwróciło wywołanie metody protected initialValue() zwracającej wartość inicjującą zmienną ThreaLocal).
W ten sposób możemy na przykład zdefiniować metodę get()
, która zwróci identyfikator bieżącego wątku:
|
|
Źródło: ThreadName.java
Jak działa ThreaLocal w środku?
Byłam przekonana, że ThreadLocal jest zaimplementowana tak, że przechowuje jakąś mapę ConcurrentHashMap<Thread,T>
używającą Thread.currentThread()
jako klucza i wartości zmiennej ThreadLocal… jako wartości. Sądziłam, że w zależności od tego, w jakim wątku się aktualnie wykonujemy, uzyskujemy (lub ustawiamy w tej mapie) odpowiednią wartość.
Tak, ryzykujemy, że przy mapie będzie dość “tłoczno” (thread contention). Dodatkowo, ponieważ zmienne ThreadLocal są statyczne, taka mapa musiałaby być również zasobem statycznym: byłaby trzymana w pamięci razem z klasą, a więc w zasadzie przez cały czas działania aplikacji. A zakończone wątki - klucze w owej mapie - wraz z ich zasobami - również nie mogłyby być zwolnione przez garbage collector.
Chyba że użylibyśmy WeakHashMap
- wówczas kolektor mógłby zgarnąć zakończony wątek (wraz z całą “dżunglą”). Ale wciaż jest problem “zagęszczenia” wokół mapy.
Tymczasem te zmienne wcale tak nie działają!
Zobaczcie:
Każdy wątek przechowuje (i ma wyłączny dostęp, a więc odpada konieczność synchronizacji) mapę, której kluczami są zmienne ThreadLocal, a wartościami - wartości tych zmiennych. Niezależnie od tego, jaką ścieżką przechodzi kod w danym wątku, wszystkie zapisywane i odczytywane przez ten kod zmienne typu ThreadLocal nie są “wyciągane” z pól obiektów, lecz z mapy siedzącej w obiekcie wątku.
|
|
Tak naprawdę używana jest wewnętrzna implementacja mapy: ThreadLocal.ThreadLocalMap
. Optymalizuje ona sposób liczenia hashy dla kluczy tak, aby nie było zbyt wielu kolizji podczas wstawiania wartości do mapy (często przecież tworzone i inicjowane jest na raz wiele zmiennych ThreadLocal z domyślnymi wartościami) i dodatkowo dba o usuwanie entriesów z nullowymi kluczami (weak referencje pomagają w tym, że garbage collector odśmieca obiekty ThreadLocal zakończonych wątków, ale entriesy z nullowymi kluczami wciąż twią w ThreadLcoalMap) głównie podczas kolizji bądź zmiany rozmiaru.
[obrazek thread local map i wątki]
Do czego używa się zmiennych ThreadLocal?
sesje
Zmienne TL (ThreadLocal) mogą być używane do przechowywania informacji specyficznych dla użytkownika, na przykład danych sesji obecnie zalogowanego użytkownika aplikacji w wielowątkowej aplikacji webowej
połączenie do bazy danych
W aplikacjach wielowątkowych każdy wątek może mieć własne połączenie do bazy danych, dzięki czemu unika się “stłoczenia” wątków podczas dostępu do bazy i poprawia się ogólną wydajność apliakcji
identyfikacja żądania
Można śledzić żądania - wątek może przechowywać w zmiennej TL identyfiakator żądania - po to, by móc skorelować logi należące do tego żądania podczas przejścia przez całą ścieżkę obsługi tego żądania w aplikacji
zarządzanie transakcjami
Zmienne TL przechowują identyfiakator transakcji czy sam obiekt reprezentujący transację/jej stan Biblioteki i frameworki które pomagają w pisaniu kodu transakcyjnego (Spring?) zwykle używają zmiennych ThreadLocal do przechowywania stanu transakcji (dlatego właśnie próba zakomitowania transakcji w innym wątu niż ten, w którym transakcja została utworzona prowadzi do błędów). Ta właściwość pozwala na zaimplementowanie transakcyjności w sposób dość “przezroczysty” dla programistów piszących kod transakcyjny, np. na zaoferowanie “declarative transactions” w bibliotece Spring.
kontekst
W zmiennych TL można przechowywać informacje kontekstowe takie jak poziom logowania bądź docelowe miejsce logowania (plik/konsola/socket)
locale
W wielowątkowych aplikacjach webowych w zmiennych TL przechowywać można informacje o preferowanym przez użytkownika (i wysłanym przez przeglądarkę w nagłówku żądania HTTP) języku i “locale” (patrz: Accept-Language)
cache
Instancje cache-a mogą być przechowywane w zmiennej TL (cache per thread) aby uniknąć “stłoczenia” wątków podczas dostępu do globalnej instancji cache-a
logowanie
Mogą być również wykorzystywane do przechowywania informacji o
- kontekście przetwarzania (rejestrowania, jakie metody biznesowe zostały wywołane)
- czasie wykonania (czas rozpoczęcia/zakończenia przetwarzania)
- identyfikacji żądania (w modelu: jeden wątek obsługuje jedno żądanie)
Pamiętam, że pierwszy raz spotkałam zmienne ThreadLocal, gdy wgryzałam się w sposób logowania żądań i związanych z nimi transakcji w dużej aplikacji typu “enterprise”. Nie wiedziałam o tym, że istnieje coś takiego jak MDC (Mapped Diagnotic Context) i jaki problem rozwiązuje (spoiler: pozwala skonfigurować logowanie tak, aby automatycznie dodawały się informacji o kontekście - jaki request, jaka sesja, jaka transakcja). Implementacje MDC w popularnych bibliotekach są oparte właśnie o zmienne ThreadLocal, na przykład:
- Logback:
- Log4J:
Scoped Values - szczegóły
Po tej przydługiej dygresji popatrzmy na to, czym jest ScopedValue. Według opisu w JavaDoc:
A value that may be safely and efficiently shared to methods without using method parameters.
W zasadzie to samo można powiedzieć o ThreadLocal! Jednak kiedy wczytamy się w opis, zauważymy, że wartości nie są “przypisywane” czy “ustawiane” (tak jak w zmiennych TL), lecz “związane” (“bound”).
API ScopedValue działa przez wywołanie metody w której obiekt ScopedValue jest “związany” (“bounded”) z pewna wartością przez “ograniczony” (“scoped”) okres, w którym ta metoda jest wykonywana (…a to “związanie” znika po wyjściu z “zakresu”).
Kontekst dynamiczny
To “związanie” wartości z pewnym ograniczonym czasem działania (a właściwie - z leksykalnym zakresem kodu, w którym związanie jest “ważne”) nazywane jest “kontekstem dynamicznym”: poza tym kontekstem zmienna nie jest związana z żadną wartością.
Można o tych zmiennych myśleć tak:
- potrzebujemy zmiennej (nazwijmy ją VAL) typu ScopedValue
(V to typ przechowywanej w ScopedValue wartości)) - potrzebujemy, aby posiadała pewną wartość v typu V
- chcemy związać zmienną VAL z wartością v podczas wykonywania pewnego runnable:
() -> process()
Wówczas te wymagania można zakodować tak:
|
|
Źródło: Scoped.java
Związanie
Ponowne związanie
Zmienne ScopedValue powinny być deklarowane jako static final
i rozumiane jako “klucze” do uzyskania wartości w kontekście, w jakim są zdefiniowane.
“Związanie” zawsze dokonuje się w kontekście bieżącego wątku - jednak może być ono “zawiązane ponownie” (“rebound”) na potrzeby wywoływanej metody: zagnieżdzamy wówczas nowy zakrez dynamiczny (utworzony po pownym związaniu) wewnątrz starego kontekstu.
|
|
W powyższym programiku:
- dla każdego napisu z wiersza poleceń tworzony jest nowy zakres (scope) dla zmiennej VAL, która jest związana (binding) z wartością typu V przechowującą napis
- w zakresie tworzony jest zakres strukturalny
StructuredTaskScope
(napiszę o nim więce w kolejnym wpisie) w którym submitowane są do wykonania (jako wątki wirtualne) dwarunnable
- w funkji
childTask(int id)
- ustawiam nazwę bieżącego wątku
- tworzę nowy zakres w którym wiążę VAR z watością V, do której dopisuję wartość parametru id
- wyousuję wartość w VAL oraz nazwę bieżącego wątku
Wiem, ten program kompletnie nie ma sensu. Ale moim celem było sprawdzenie, czy mam w głowie właściwy model tego, co się będzie działo :)
Źródło: ScopedThread.java