Jak zaimplementowano ThreadLocal? Czy jest zaimplementowany w Javie (przy użyciu pewnej współbieżnej mapy z ThreadID do obiektu), czy może używa jakiegoś haka JVM, aby zrobić to bardziej wydajnie?
Odpowiedzi:
Wszystkie odpowiedzi tutaj są poprawne, ale trochę rozczarowujące, ponieważ nieco przemilczają, jak sprytna ThreadLocal
jest implementacja. Właśnie patrzyłem na kod źródłowy programuThreadLocal
i byłem pod wrażeniem sposobu jego implementacji.
Naiwna implementacja
ThreadLocal<T>
Co byś zrobił, gdybym poprosił Cię o zaimplementowanie klasy posiadającej API opisane w javadoc? Początkowa implementacja prawdopodobnie będzie kluczem do ConcurrentHashMap<Thread,T>
użycia Thread.currentThread()
. To działałoby dość dobrze, ale ma pewne wady.
ConcurrentHashMap
to całkiem sprytna klasa, ale ostatecznie nadal musi radzić sobie z zapobieganiem w jakikolwiek sposób wielu wątków, a jeśli różne wątki będą go regularnie uderzać, wystąpią spowolnienia.Implementacja przyjazna dla GC
Ok, spróbuj ponownie, zajmijmy się problemem czyszczenia pamięci, używając słabych referencji . Radzenie sobie z WeakReferences może być mylące, ale powinno wystarczyć użycie mapy zbudowanej w ten sposób:
Collections.synchronizedMap(new WeakHashMap<Thread, T>())
Lub jeśli używamy guawy (a powinniśmy!):
new MapMaker().weakKeys().makeMap()
Oznacza to, że gdy nikt inny nie trzyma wątku (co oznacza, że jest skończony), klucz / wartość może zostać zebrany jako śmieci, co jest ulepszeniem, ale nadal nie rozwiązuje problemu rywalizacji o wątek, co oznacza, że jak dotąd nasze ThreadLocal
nie wszystko niesamowita klasa. Co więcej, jeśli ktoś zdecydowałby się trzymać Thread
przedmioty po ich zakończeniu, nigdy nie otrzymałyby one GC, a zatem również naszych obiektów, mimo że są one obecnie technicznie nieosiągalne.
Sprytna implementacja
Myśleliśmy o ThreadLocal
mapowaniu wątków na wartości, ale może to nie jest właściwy sposób myślenia o tym. Zamiast myśleć o tym jako o mapowaniu ThreadLocal na wartości w każdym obiekcie ThreadLocal, co by było, gdybyśmy pomyśleli o tym jako o mapowaniu obiektów ThreadLocal na wartości w każdym ThreadLocal ? Jeśli każdy wątek przechowuje mapowanie, a ThreadLocal po prostu zapewnia ładny interfejs do tego mapowania, możemy uniknąć wszystkich problemów z poprzednimi implementacjami.
Implementacja wyglądałaby mniej więcej tak:
// called for each thread, and updated by the ThreadLocal instance
new WeakHashMap<ThreadLocal,T>()
Nie musisz się tutaj martwić o współbieżność, ponieważ tylko jeden wątek będzie miał kiedykolwiek dostęp do tej mapy.
Programiści Java mają tutaj nad nami dużą przewagę - mogą bezpośrednio rozwijać klasę Thread i dodawać do niej pola i operacje, i właśnie to zrobili.
W java.lang.Thread
środku znajdują się następujące wiersze:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
Co, jak sugeruje komentarz, jest rzeczywiście prywatnym mapowaniem pakietów wszystkich wartości śledzonych przez ThreadLocal
obiekty w tym celu Thread
. Implementacja ThreadLocalMap
nie jest WeakHashMap
, ale przebiega zgodnie z tą samą podstawową umową, w tym trzymaniem kluczy przez słabe odniesienie.
ThreadLocal.get()
jest następnie implementowany w następujący sposób:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
I ThreadLocal.setInitialValue()
tak:
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
Zasadniczo użyj mapy w tym wątku do przechowywania wszystkich naszych ThreadLocal
obiektów. W ten sposób nigdy nie musimy martwić się o wartości w innych wątkach ( ThreadLocal
dosłownie mamy dostęp tylko do wartości w bieżącym wątku) i dlatego nie mamy problemów ze współbieżnością. Co więcej, po zakończeniu Thread
, jego mapa zostanie automatycznie poddana GC, a wszystkie lokalne obiekty zostaną wyczyszczone. Nawet jeśli Thread
jest trzymany, ThreadLocal
obiekty są trzymane przez słabe odniesienie i można je wyczyścić, gdy tylko ThreadLocal
obiekt znajdzie się poza zasięgiem.
Nie trzeba dodawać, że byłem pod wrażeniem tej implementacji, dość elegancko rozwiązuje wiele problemów związanych ze współbieżnością (wprawdzie dzięki byciu częścią rdzenia Javy, ale jest to wybaczalne, ponieważ jest to tak sprytna klasa) i pozwala na szybkie i bezpieczny wątkowo dostęp do obiektów, do których na raz wystarczy dostęp tylko przez jeden wątek.
ThreadLocal
Implementacja tl; dr jest całkiem fajna i znacznie szybsza / inteligentniejsza niż mogłoby się wydawać na pierwszy rzut oka.
Jeśli spodobała Ci się ta odpowiedź, możesz również docenić moją (mniej szczegółową) dyskusję na tematThreadLocalRandom
.
Thread
/ ThreadLocal
fragmenty kodu pobrane z implementacji Java 8 w Oracle / OpenJDK .
WeakHashMap<String,T>
wprowadza kilka problemów, nie jest wątkowo bezpieczny i „jest przeznaczony głównie do użytku z kluczowymi obiektami, których metody equals testują tożsamość obiektu przy użyciu operatora ==” - więc użycie Thread
obiektu jako klucza byłoby lepsze. Sugerowałbym użycie mapy słabe klawisze Guava, którą opisałem powyżej, dla twojego przypadku użycia.
Thread.exit()
jest wywoływany i zobaczysz threadLocals = null;
tam. Komentarz odnosi się do tego błędu, który również może ci się spodobać .
Masz na myśli java.lang.ThreadLocal
. To całkiem proste, naprawdę, to tylko mapa par nazwa-wartość przechowywana w każdym Thread
obiekcie (patrz Thread.threadLocals
pole). Interfejs API ukrywa ten szczegół implementacji, ale to mniej więcej wszystko.
Zmienne ThreadLocal w Javie działają poprzez dostęp do HashMap przechowywanej przez instancję Thread.currentThread ().
Załóżmy, że zamierzasz zaimplementować ThreadLocal
, jak sprawisz, że będzie to specyficzne dla wątku? Oczywiście najprostszą metodą jest utworzenie niestatycznego pola w klasie Thread, nazwijmy to threadLocals
. Ponieważ każdy wątek jest reprezentowany przez wystąpienie wątku, więc threadLocals
każdy wątek również byłby inny. I to właśnie robi Java:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
Co ThreadLocal.ThreadLocalMap
tu jest ? Ponieważ masz tylko threadLocals
jeden wątek, więc jeśli po prostu przyjmiesz threadLocals
jako swój ThreadLocal
(powiedzmy, zdefiniujesz threadLocals jako Integer
), będziesz mieć tylko jeden ThreadLocal
dla określonego wątku. A co, jeśli chcesz mieć wiele ThreadLocal
zmiennych dla wątku? Najprościej jest zrobić threadLocals
a HashMap
, key
każdy wpis jest nazwą ThreadLocal
zmiennej, a value
każdy wpis jest wartością ThreadLocal
zmiennej. Trochę zagmatwane? Powiedzmy, że mamy dwa wątki t1
i t2
. przyjmują to samo Runnable
wystąpienie co parametr Thread
konstruktora i obie mają dwie ThreadLocal
zmienne o nazwie tlA
i tlb
. Tak to jest.
t1.tlA
+-----+-------+
| Key | Value |
+-----+-------+
| tlA | 0 |
| tlB | 1 |
+-----+-------+
t2.tlB
+-----+-------+
| Key | Value |
+-----+-------+
| tlA | 2 |
| tlB | 3 |
+-----+-------+
Zauważ, że wartości są tworzone przeze mnie.
Teraz wydaje się idealny. Ale co to jest ThreadLocal.ThreadLocalMap
? Dlaczego po prostu nie użył HashMap
? Aby rozwiązać problem, zobaczmy, co się stanie, gdy ustawimy wartość set(T value)
metodą ThreadLocal
klasy:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
getMap(t)
po prostu wraca t.threadLocals
. Ponieważ t.threadLocals
został zainicjowany null
, więc wchodzimy createMap(t, value)
pierwszy:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
Tworzy nową ThreadLocalMap
instancję, używając bieżącej ThreadLocal
instancji i wartości do ustawienia. Zobaczmy, ThreadLocalMap
jak to jest, w rzeczywistości jest częścią ThreadLocal
klasy
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
...
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
...
}
Rdzeniem tej ThreadLocalMap
klasy jest Entry class
rozszerzenie WeakReference
. Zapewnia to, że jeśli bieżący wątek zostanie zamknięty, zostanie automatycznie usunięty jako śmieci. Dlatego używa ThreadLocalMap
zamiast prostego pliku HashMap
. Przekazuje prąd ThreadLocal
i jego wartość jako parametr Entry
klasy, więc gdy chcemy pobrać wartość, możemy ją pobrać z table
, która jest instancją Entry
klasy:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
Tak wygląda cały obraz:
Koncepcyjnie możesz myśleć o a ThreadLocal<T>
jako o trzymaniuMap<Thread,T>
które przechowuje wartości specyficzne dla wątku, chociaż nie jest to sposób, w jaki jest faktycznie implementowany.
Wartości specyficzne dla wątku są przechowywane w samym obiekcie Thread; gdy wątek się kończy, wartości specyficzne dla wątku mogą zostać usunięte.