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 ThreadLocaljest 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.
ConcurrentHashMapto 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 ThreadLocalnie wszystko niesamowita klasa. Co więcej, jeśli ktoś zdecydowałby się trzymać Threadprzedmioty 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 ThreadLocalmapowaniu 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 ThreadLocalobiekty w tym celu Thread. Implementacja ThreadLocalMapnie 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 ThreadLocalobiektów. W ten sposób nigdy nie musimy martwić się o wartości w innych wątkach ( ThreadLocaldosł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 Threadjest trzymany, ThreadLocalobiekty są trzymane przez słabe odniesienie i można je wyczyścić, gdy tylko ThreadLocalobiekt 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.
ThreadLocalImplementacja 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/ ThreadLocalfragmenty 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 Threadobiektu 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 Threadobiekcie (patrz Thread.threadLocalspole). 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 threadLocalskaż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.ThreadLocalMaptu jest ? Ponieważ masz tylko threadLocalsjeden wątek, więc jeśli po prostu przyjmiesz threadLocalsjako swój ThreadLocal(powiedzmy, zdefiniujesz threadLocals jako Integer), będziesz mieć tylko jeden ThreadLocaldla określonego wątku. A co, jeśli chcesz mieć wiele ThreadLocalzmiennych dla wątku? Najprościej jest zrobić threadLocalsa HashMap, keykażdy wpis jest nazwą ThreadLocalzmiennej, a valuekażdy wpis jest wartością ThreadLocalzmiennej. Trochę zagmatwane? Powiedzmy, że mamy dwa wątki t1i t2. przyjmują to samo Runnablewystąpienie co parametr Threadkonstruktora i obie mają dwie ThreadLocalzmienne o nazwie tlAi 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ą ThreadLocalklasy:
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.threadLocalszostał zainicjowany null, więc wchodzimy createMap(t, value)pierwszy:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
Tworzy nową ThreadLocalMapinstancję, używając bieżącej ThreadLocalinstancji i wartości do ustawienia. Zobaczmy, ThreadLocalMapjak to jest, w rzeczywistości jest częścią ThreadLocalklasy
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 ThreadLocalMapklasy jest Entry classrozszerzenie WeakReference. Zapewnia to, że jeśli bieżący wątek zostanie zamknięty, zostanie automatycznie usunięty jako śmieci. Dlatego używa ThreadLocalMapzamiast prostego pliku HashMap. Przekazuje prąd ThreadLocali jego wartość jako parametr Entryklasy, więc gdy chcemy pobrać wartość, możemy ją pobrać z table, która jest instancją Entryklasy:
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.