Wie wird Javas ThreadLocal unter der Haube implementiert?

80

Wie ist ThreadLocal implementiert? Ist es in Java implementiert (unter Verwendung einer gleichzeitigen Zuordnung von ThreadID zu Objekt) oder verwendet es einen JVM-Hook, um es effizienter zu machen?

ripper234
quelle

Antworten:

119

Alle Antworten hier sind richtig, aber ein wenig enttäuschend, da sie etwas beschönigen, wie clever ThreadLocaldie Implementierung ist. Ich habe mir nur den Quellcode angesehenThreadLocal und war angenehm beeindruckt von der Implementierung.

Die naive Implementierung

ThreadLocal<T>Was würden Sie tun, wenn ich Sie bitten würde, eine Klasse mit der im Javadoc beschriebenen API zu implementieren ? Eine erste Implementierung wäre wahrscheinlich eine ConcurrentHashMap<Thread,T>Verwendung Thread.currentThread()als Schlüssel. Dies würde einigermaßen gut funktionieren, hat jedoch einige Nachteile.

  • Thread-Konkurrenz - ConcurrentHashMapist eine ziemlich kluge Klasse, aber sie muss sich letztendlich immer noch damit befassen, zu verhindern, dass mehrere Threads in irgendeiner Weise damit herumspielen, und wenn verschiedene Threads sie regelmäßig treffen, kommt es zu Verlangsamungen.
  • Behält dauerhaft einen Zeiger sowohl auf den Thread als auch auf das Objekt, auch nachdem der Thread beendet wurde und GC'ed werden könnte.

Die GC-freundliche Implementierung

Ok, versuchen Sie es noch einmal. Lassen Sie uns das Problem der Speicherbereinigung mithilfe schwacher Referenzen lösen . Der Umgang mit WeakReferences kann verwirrend sein, sollte jedoch ausreichen, um eine Karte zu verwenden, die wie folgt erstellt wurde:

 Collections.synchronizedMap(new WeakHashMap<Thread, T>())

Oder wenn wir Guave benutzen (und wir sollten es sein!):

new MapMaker().weakKeys().makeMap()

Dies bedeutet, dass, sobald niemand anderes am Thread festhält (was bedeutet, dass er fertig ist), der Schlüssel / Wert durch Müll gesammelt werden kann. Dies ist eine Verbesserung, behebt jedoch immer noch nicht das Problem mit den Thread-Konflikten, was bedeutet, dass dies bisher ThreadLocalnicht alles ist erstaunlich von einer Klasse. Wenn sich jemand dazu entschließen würde, an ThreadObjekten festzuhalten , nachdem er fertig war, würde er niemals GC'ed werden, und daher würden unsere Objekte auch nicht, obwohl sie jetzt technisch nicht erreichbar sind.

Die clevere Implementierung

Wir haben darüber nachgedacht, ThreadLocalThreads Werten zuzuordnen, aber vielleicht ist das nicht der richtige Weg, darüber nachzudenken. Anstatt es als Zuordnung von Threads zu Werten in jedem ThreadLocal-Objekt zu betrachten, was wäre, wenn wir es als Zuordnung von ThreadLocal-Objekten zu Werten in jedem Thread betrachten würden ? Wenn jeder Thread das Mapping speichert und ThreadLocal lediglich eine nette Schnittstelle zu diesem Mapping bietet, können wir alle Probleme der vorherigen Implementierungen vermeiden.

Eine Implementierung würde ungefähr so ​​aussehen:

// called for each thread, and updated by the ThreadLocal instance
new WeakHashMap<ThreadLocal,T>()

Hier besteht kein Grund zur Sorge, da nur ein Thread jemals auf diese Karte zugreifen wird.

Die Java-Entwickler haben hier einen großen Vorteil gegenüber uns - sie können die Thread-Klasse direkt entwickeln und Felder und Operationen hinzufügen, und genau das haben sie getan.

In java.lang.Threadgibt es die folgenden Zeilen:

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

Was, wie der Kommentar andeutet, tatsächlich eine paketprivate Zuordnung aller Werte ist, die von ThreadLocalObjekten dafür verfolgt werden Thread. Die Implementierung von ThreadLocalMapist kein WeakHashMap, aber es folgt dem gleichen Grundvertrag, einschließlich des Haltens seiner Schlüssel durch schwache Referenz.

ThreadLocal.get() wird dann wie folgt implementiert:

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();
}

Und ThreadLocal.setInitialValue()so:

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;
}

Verwenden Sie im Wesentlichen eine Karte in diesem Thread , um alle unsere ThreadLocalObjekte aufzunehmen. Auf diese Weise müssen wir uns nie um die Werte in anderen Threads kümmern (können ThreadLocalbuchstäblich nur auf die Werte im aktuellen Thread zugreifen) und haben daher keine Probleme mit der Parallelität. Sobald Threaddies erledigt ist, wird die Karte automatisch GC-geprüft und alle lokalen Objekte werden bereinigt. Selbst wenn das Objekt Threadfestgehalten wird, werden die ThreadLocalObjekte durch schwache Referenz gehalten und können bereinigt werden, sobald das ThreadLocalObjekt den Gültigkeitsbereich verlässt.


Unnötig zu erwähnen, dass ich von dieser Implementierung ziemlich beeindruckt war. Sie umgeht auf elegante Weise viele Probleme mit der Parallelität (zwar durch die Nutzung des Java-Kerns, aber das ist verzeihlich, da es sich um eine so clevere Klasse handelt) und ermöglicht schnell und schnell Thread-sicherer Zugriff auf Objekte, auf die jeweils nur ein Thread zugreifen muss.

Die ThreadLocal Implementierung von tl; dr ist ziemlich cool und viel schneller / intelligenter als Sie vielleicht auf den ersten Blick denken.

Wenn Ihnen diese Antwort gefallen hat, werden Sie vielleicht auch meine (weniger detaillierte) Diskussion überThreadLocalRandom schätzen .

Thread/ ThreadLocalCode-Schnipsel aus der Implementierung von Java 8 durch Oracle / OpenJDK .

dimo414
quelle
1
Ihre Antworten sehen fantastisch aus, aber es ist momentan zu lang für mich. +1 und akzeptiert, und ich füge es meinem getpocket.com-Konto hinzu, um es später zu lesen. Vielen Dank!
Ripper234
Ich benötige ein ThreadLocal-ähnliches Element, mit dem ich auch auf die vollständige Liste der Werte zugreifen kann, ähnlich wie bei map.values ​​(). Meine naive Implementierung ist also eine WeakHashMap <String, Object>, deren Schlüssel Thread.currentThread (). GetName () ist. Dies vermeidet den Verweis auf den Thread selbst. Wenn der Thread verschwindet, hält nichts mehr den Namen des Threads (eine Annahme, gebe ich zu) und mein Wert wird verschwinden.
Bmauter
Ich habe vor kurzem eine entsprechende Frage beantwortet . A WeakHashMap<String,T>führt mehrere Probleme ein, ist nicht threadsicher und "hauptsächlich für die Verwendung mit Schlüsselobjekten vorgesehen, deren Methoden mit dem Operator == auf Objektidentität getestet werden" - daher Threadwäre es besser , das Objekt tatsächlich als Schlüssel zu verwenden. Ich würde vorschlagen, die oben beschriebene Guava schwachKeys-Karte für Ihren Anwendungsfall zu verwenden.
dimo414
1
Nun, die schwachen Schlüssel sind nicht erforderlich, aber erwägen Sie die Verwendung einer ConcurrentHashMap über eine synchronisierte HashMap - die erstere ist für den Multithread-Zugriff ausgelegt und funktioniert viel besser, wenn jeder Thread im Allgemeinen auf einen anderen Schlüssel zugreift.
dimo414
1
@shmosel Diese Klasse ist stark abgestimmt, daher würde ich davon ausgehen, dass solche Überlegungen angestellt wurden. Ein kurzer Blick zeigt, dass wenn ein Thread normal beendet Thread.exit()wird, aufgerufen wird und Sie threadLocals = null;genau dort sehen werden. Ein Kommentar verweist auf diesen Fehler, den Sie möglicherweise auch gerne lesen.
dimo414
33

Du meinst java.lang.ThreadLocal. Eigentlich ist es ganz einfach, es ist nur eine Karte von Name-Wert-Paaren, die in jedem ThreadObjekt gespeichert sind (siehe Thread.threadLocalsFeld). Die API verbirgt diese Implementierungsdetails, aber das ist mehr oder weniger alles, was dazu gehört.

Skaffman
quelle
Ich kann nicht verstehen, warum welche benötigt werden sollten, da die Daten per Definition nur für einen einzelnen Thread sichtbar sind.
Skaffman
8
Richtig, es gibt keine Synchronisation oder Sperrung um oder innerhalb der ThreadLocalMap, da nur im Thread darauf zugegriffen wird.
Cowan
8

ThreadLocal-Variablen in Java greifen auf eine HashMap zu, die von der Thread.currentThread () -Instanz gehalten wird.

Chris Vest
quelle
Das ist nicht richtig (oder zumindest nicht mehr). Thread.currentThread () ist ein nativer Aufruf in der Thread.class. Thread hat auch eine "ThreadLocalMap", die ein einzelner Bucket (Array) von Hash-Codes ist. Dieses Objekt unterstützt die Map-Schnittstelle nicht.
user924272
1
Das habe ich im Grunde gesagt. currentThread () gibt eine Thread-Instanz zurück, die eine Zuordnung von ThreadLocals zu Werten enthält.
Chris Vest
4

Angenommen, Sie werden implementieren ThreadLocal. Wie machen Sie es threadspezifisch? Die einfachste Methode besteht natürlich darin, ein nicht statisches Feld in der Thread-Klasse zu erstellen. Nennen wir es threadLocals. Da jeder Thread durch eine Thread-Instanz dargestellt wird, wäre dies auch threadLocalsin jedem Thread anders. Und das macht Java auch:

/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

Was ist ThreadLocal.ThreadLocalMaphier Da Sie nur eine threadLocalsfür einen Thread haben, haben Sie nur eine für einen bestimmten Thread, wenn Sie sie einfach threadLocalsals Ihre ThreadLocal(z. B. definieren Sie threadLocals als Integer) nehmen ThreadLocal. Was ist, wenn Sie mehrere ThreadLocalVariablen für einen Thread möchten ? Der einfachste Weg ist , um threadLocalsein HashMap, die keyvon jedem Eintrag ist der Name des ThreadLocalVariable, und die valuevon jedem Eintrag ist der Wert der ThreadLocalVariablen. Ein bisschen verwirrend? Nehmen wir an, wir haben zwei Threads t1und t2. Sie verwenden dieselbe RunnableInstanz wie der Parameter des ThreadKonstruktors und haben beide zwei ThreadLocalVariablen mit dem Namen tlAund tlb. So ist es.

t1.tlA

+-----+-------+
| Key | Value |
+-----+-------+
| tlA |     0 |
| tlB |     1 |
+-----+-------+

t2.tlB

+-----+-------+
| Key | Value |
+-----+-------+
| tlA |     2 |
| tlB |     3 |
+-----+-------+

Beachten Sie, dass die Werte von mir gemacht werden.

Jetzt scheint es perfekt zu sein. Aber was ist das ThreadLocal.ThreadLocalMap? Warum wurde es nicht einfach benutzt HashMap? Um das Problem zu lösen, schauen wir uns an, was passiert, wenn wir einen Wert über die set(T value)Methode der ThreadLocalKlasse festlegen :

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)kehrt einfach zurück t.threadLocals. Weil t.threadLocalswurde initiiert null, also geben wir createMap(t, value)zuerst ein:

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

Es wird eine neue ThreadLocalMapInstanz mit der aktuellen ThreadLocalInstanz und dem einzustellenden Wert erstellt. Mal sehen, wie es ThreadLocalMapist, es ist tatsächlich Teil der ThreadLocalKlasse

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);
    }

    ...

}

Der Kern der ThreadLocalMapKlasse ist der Entry class, der sich erstreckt WeakReference. Es stellt sicher, dass beim Beenden des aktuellen Threads automatisch Müll gesammelt wird. Aus diesem Grund wird ThreadLocalMapanstelle eines einfachen verwendet HashMap. Es übergibt den aktuellen ThreadLocalWert und seinen Wert als Parameter der EntryKlasse. Wenn wir also den Wert abrufen möchten, können wir ihn von tableeiner Instanz der EntryKlasse abrufen:

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();
}

So sieht das ganze Bild aus:

Das ganze Bild

Searene
quelle
-1

Konzeptionell kann man sich a ThreadLocal<T>als Holding a Map<Thread,T>vorstellen, in dem die threadspezifischen Werte gespeichert sind, obwohl dies nicht der Fall ist.

Die threadspezifischen Werte werden im Thread-Objekt selbst gespeichert. Wenn der Thread beendet wird, können die threadspezifischen Werte durch Müll gesammelt werden.

Referenz: JCIP

bpjoshi
quelle
1
Konzeptionell ja. Wie Sie jedoch anhand anderer Antworten oben sehen, ist die Implementierung völlig anders.
Archit