Jak zaimplementowano ThreadLocal w Javie?

81

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?

zrywak234
źródło

Odpowiedzi:

120

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.

  • Konkurencja wątków - 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.
  • Trwale utrzymuje wskaźnik zarówno do wątku, jak i do obiektu, nawet po zakończeniu wątku i można go wykonać metodą GC.

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 .

dimo414
źródło
1
Twoje odpowiedzi wyglądają niesamowicie, ale teraz jest dla mnie za długie. +1 i zaakceptowano. Dodaję go do mojego konta getpocket.com, aby przeczytać później. Dzięki!
ripper234
Potrzebuję elementu podobnego do ThreadLocal, który również umożliwi mi dostęp do pełnej listy wartości, podobnie jak map.values ​​(). Tak więc moja naiwna implementacja to WeakHashMap <String, Object>, gdzie kluczem jest Thread.currentThread (). GetName (). Pozwala to uniknąć odniesienia do samego wątku. Jeśli wątek zniknie, nic już nie trzyma nazwy wątku (założenie, przyznaję) i moja wartość zniknie.
bmauter
Właściwie całkiem niedawno odpowiedziałem na to pytanie . A 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.
dimo414
1
Cóż, słabe klucze nie są konieczne, ale rozważ użycie ConcurrentHashMap zamiast zsynchronizowanej HashMap - ten pierwszy jest przeznaczony do dostępu wielowątkowego i będzie działał znacznie lepiej w przypadku, gdy każdy wątek generalnie uzyskuje dostęp do innego klucza.
dimo414
1
@shmosel ta klasa jest mocno dostrojona, więc zacznę od założenia, że ​​takie rozważania zostały poczynione. Szybki rzut oka pokazuje, że gdy wątek kończy się normalnie, 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ć .
dimo414
33

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.

skaffman
źródło
Nie rozumiem, dlaczego jakiekolwiek miałyby być potrzebne, biorąc pod uwagę, że z definicji dane są widoczne tylko dla jednego wątku.
skaffman
8
Prawidłowo, nie ma synchronizacji ani blokowania wokół ThreadLocalMap, ponieważ jest on dostępny tylko w wątku.
Cowan
8

Zmienne ThreadLocal w Javie działają poprzez dostęp do HashMap przechowywanej przez instancję Thread.currentThread ().

Chris Vest
źródło
To nie jest poprawne (a przynajmniej już nie jest). Thread.currentThread () to natywne wywołanie w Thread.class. Wątek ma również „ThreadLocalMap”, który jest pojedynczym zasobnikiem (tablicą) kodów skrótów. Ten obiekt nie obsługuje interfejsu Map.
user924272
1
Tak w zasadzie powiedziałem. currentThread () zwraca wystąpienie Thread, które przechowuje mapę ThreadLocals na wartości.
Chris Vest
4

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:

Całe zdjęcie

Searene
źródło
-1

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.

Źródła : JCIP

bpjoshi
źródło
1
Koncepcyjnie tak. Ale jak widać z innych odpowiedzi powyżej, implementacja jest zupełnie inna.
Architekci