2.9 Mit dem Thread verbundene Variablen *
Unterschiedliche Threads können ohne Probleme das gleiche Runnable ausführen, was auch ein übliches Szenario ist, wenn der Programmcode immer der gleiche ist. Doch auch wenn der Programmcode immer gleich bleibt, soll jedem Thread doch vielleicht ein eigener Speicherbereich zugeteilt werden, in dem er Informationen ablegen kann. Falls das Runnable-Objekt selbst eine Objektvariable hätte und pro Thread auf diese Weise ein neues Runnable-Objekt gebildet würde, wäre das kein Problem, doch müsste bei genau einem Runnable-Objekt und beliebig vielen Threads ein anderer Ort gefunden werden.
2.9.1 ThreadLocal
Die Lösung ist relativ einfach: Es müsste eine Datenstruktur geben, die jeden laufenden Thread mit einem Objekt oder einer Datenstruktur assoziiert. Diese Struktur muss dann aus dem Runnable-Objekt heraus referenzierbar sein. Java bietet mit der Klasse java.lang.ThreadLocal eine Implementierung für thread-lokale Variablen (engl. thread-local storage [TLS]) an.
Abbildung 2.12: UML-Diagramm von ThreadLocal
class java.lang.ThreadLocal<T> |
- ThreadLocal()
Erzeugt eine neue thread-lokale Variable. - void set(T value)
Setzt den Wert für den lokalen Thread. - T get()
Liefert den Wert, der mit dem aktuellen Thread verbunden ist. - protected T initialValue()
Die Methode kann überschrieben werden und liefert dann den Anfangswert beim ersten Aufruf von get() ohne vorangehendes set(). Der Standard ist null. - void remove()
Entfernt den Wert der thread-lokalen Variablen, um etwa Speicher freizumachen. Ein anschließendes get() liefert wieder den Wert aus initialValue().
Zählen mit thread-lokalen Variablen
Das folgende Beispiel soll ein Runnable definieren, das endlos zählt. Der Clou ist aber, dass der Zähler keine lokale Variable, sondern eine thread-lokale Variable ist, die ThreadLocal verwaltet. Drei Threads sollen das gleiche Runnable abarbeiten: zwei neue Threads und der aktuell ausführende main-Thread:
Listing 2.41: com/tutego/insel/thread/ThreadLocalDemo.java
package com.tutego.insel.thread;
public class ThreadLocalDemo
{
public static void main( String[] args )
{
Runnable runnable = new SimpleRunnable();
new Thread( runnable ).start();
new Thread( runnable ).start();
runnable.run();
}
}
class SimpleRunnable implements Runnable
{
private static final ThreadLocal<Integer> mem =
new ThreadLocal<Integer>()
{
@Override protected Integer initialValue() { return 1; }
};
@Override public void run()
{
while ( true )
{
System.out.println( Thread.currentThread().getName() +
", " + mem.get() );
mem.set( mem.get() + 1 );
}
}
}
Den Startwert legt die überschriebene Methode initialValue() mit 1 fest. Da ThreadLocal einen Zähler speichert, ist der Datencontainer mit Integer typisiert.
Die Ausgabe kann beginnen mit:
main, 1
main, 2
main, 3
main, 4
main, 5
Thread-0, 1
Thread-1, 1
Thread-0, 2
Thread-1, 2
Thread-0, 3
Thread-1, 3
Entwicklungsumgebungen |
Die IDE IntelliJ bietet die komfortable Möglichkeit, statische referenzierte nicht thread-sichere Objekte in ThreadLocal-Objekte zu konvertieren. Informationen dazu gibt es unter http://blogs.jetbrains.com/idea/2009/10/threadlocal-in-one-click/. |
remove() nutzen
Jeder Thread speichert einen Assoziativspeicher mit allen thread-lokalen Variablen. Damit im ThreadLocal vergessene Werte nicht zum Problem werden, sollten sie mit remove() wieder entfernt werden. Sonst entstehen langlebige Objektreferenzen, die zu einem Speicherleck führen können. Denn in heutigen Umgebungen werden Threads nicht einfach so gestartet und beendet, sondern leben fast schon ewig in einem Thread-Pool und werden immer wieder recycelt. Der Tipp, der sich daraus ableitet, ist der folgende: Wenn thread-lokale Variablen nicht mehr benötigt werden, sollten sie entfernt werden, denn werden sie es nicht, verstopfen sie den Speicher. Das Problem ist nur, dass ein Thread im Thread-Pool ja gar nicht weiß, was für ein Runnable er abarbeitet, sodass es schwierig ist, Einträge wieder zu entfernen.[13](Sun hat das viele Jahre auch nicht vorgesehen. ThreadLocal gibt es seit Java 1.2, aber remove() erst seit Java 5.)
2.9.2 InheritableThreadLocal
Jeder Thread kann einen neuen Thread bilden, der dann Kind ist. Da die statische main-Methode selbst von einem Haupt-Thread ausgeführt wird, wie das Beispiel zeigt, ist schon dieser Thread der Vater, der neue Unter-Threads bildet.
Mit der Klasse InheritableThreadLocal, die eine Unterklasse von ThreadLocal ist, kann das Kind vom Vater den gespeicherten Wert übernehmen. Das würde sonst nicht funktionieren, da der neue Thread ja ganz eigene Werte hat, aber mit InheritableThreadLocal bleibt dieser Wert erhalten und wird auf die Kinder vererbt.
Dass ein gestarteter Kind-Thread den Wert vom Vater-Thread übernimmt, zeigt das folgende Programm. Ein Thread gibt den geerbten Wert aus und setzt anschließend einen neuen Wert für ein Kind, das der Thread in die Welt entlässt:
Listing 2.42: com/tutego/insel/thread/InheritableThreadLocalDemo.java
package com.tutego.insel.thread;
public class InheritableThreadLocalDemo
{
public static void main( String[] args )
{
new InheritingThread().start();
}
}
class InheritingThread extends Thread
{
// private static final ThreadLocal<String> mem = new ThreadLocal<String>();
private static final InheritableThreadLocal<String> mem =
new InheritableThreadLocal<String>();
@Override public void run()
{
System.out.println( Thread.currentThread() + " bekommt " + mem.get() );
mem.set( Thread.currentThread().getName() );
new InheritingThread().start();
}
}
Die Ausgabe beginnt mit:
Thread[Thread-0,5,main] bekommt null
Thread[Thread-1,5,main] bekommt Thread-0
Thread[Thread-2,5,main] bekommt Thread-1
Thread[Thread-3,5,main] bekommt Thread-2
Der erste Thread bekommt noch nichts von seinem Vater-Thread. Da jedoch der erste Thread den eigenen Namen in den Speicher von InheritableThreadLocal legt und dann der zweite neu gestartete Kind-Thread auf den Wert zugreift, empfängt er die Zeichenfolge »Thread-0«.
Wer zum Testen InheritableThreadLocal durch ThreadLocal ersetzt, der wird merken, dass das Beispiel so nicht funktioniert.
2.9.3 ThreadLocalRandom als Zufallszahlengenerator
Zufallszahlen sind immer nur Pseudozufallszahlen und werden mit einer mathematischen Formel aus dem Vorgänger generiert. Der Vorgänger muss dabei gespeichert werden, und das ist die Aufgabe eines Random-Objekts. Die Methode Math.random() nutzt intern ein Random-Objekt, und jetzt kann es zu Wartezeiten kommen, wenn mehrere Threads gleichzeitig random() aufrufen, denn die Methode darf intern ja nur einen Thread die letzte Zufallszahl schreiben lassen. Um Zufallszahlen schnell generieren zu können, sind diese Verzögerungen ungünstig, und es gibt zwei Lösungen dafür. Einmal lässt sich pro Thread ein Random-Objekt generieren, sodass es im Code der Random-Klasse dann keine Konkurrenzsituation geben kann. Aber optimal ist das noch nicht, denn der Programmcode der Random-Klasse ist auf diese Nebenläufigkeit vorbereitet, und besser wäre ein schlankerer Programmcode, der für eine single-threaded Abarbeitung optimiert ist. Und hier kommt die unter Java 7 eingeführte Klasse java.util.concurrent.ThreadLocalRandom ins Spiel. Sie ist eine Unterklasse von Random und überschreibt die nextXXX()-Methoden so, dass es keine Synchronisation gibt; der Code ist dementsprechend schnell. Um etwa Zufallszahlen zwischen 1 und 10 zu erzeugen, schreiben wir mit der neuen Klasse:
Listing 2.43: com/tutego/insel/thread/ThreadLocalRandomDemo.java
ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
System.out.println( threadLocalRandom.nextInt( 1, 10 + 1 ) );
System.out.println( threadLocalRandom.nextInt( 1, 10 + 1 ) );
2.9.4 ThreadLocal bei der Performance-Optimierung
ThreadLocal ist eine schöne Klasse zur Performance-Optimierung. Sollen zum Beispiel zwei Threads große Datenmengen über einen zentralen Puffer lesen, müssten sie an diesem Puffer synchronisiert werden. Mit ThreadLocal kann je ein Puffer beim Thread gespeichert sein, und die Synchronisation kann entfallen. Da es im Allgemeinen wenige Threads gibt, ist die Anzahl paralleler Puffer klein. Performance-Interessierte können einen Blick auf StringCoding werfen, wie sie ThreadLocal beim Caching von (De)Kodierer-Objekten nutzt. Die Klasse String nutzt StringCoding bei der Byte/Zeichen-Kodierung. Verschweigen dürfen wir aber nicht, dass der Zugriff auch etwas kostet, sodass ein kleiner synchronisierter Bereich durchaus schneller sein kann.
Dazu noch ein Punkt zum Caching: Wir hatten gesagt, dass sich gecachte Objekte sehr gut in einem ThreadLocal ablegen lassen, insbesondere dann, wenn diese Objekte teuer im Aufbau sind. Hier müssen wir aber vorsichtig sein. Ein Szenario: Ein Server nutzt 100 Threads für Anfragen. Es kommt eine Anfrage herein, ein Thread übernimmt sie und setzt ein zu cachendes Objekt in den ThreadLocal. Das Dumme ist, dass die nächste Anfrage aber nicht wieder bei diesem Thread landen muss, denn es gibt ja 99 andere Threads, und die haben den gecachten Wert nicht im ThreadLocal. Also machen die das Gleiche, und es endet nach einer Zeit damit, dass alle 100 Threads dieses Objekt in einem lokalen Cache haben. Diese Herangehensweise kostet viel Speicher, kann aber intelligent sein, da so eine Synchronisation unnötig wird. Ist Speicher satt vorhanden und sind die Objekte klein und nicht thread-sicher, ist das eine gute Philosophie, ansonsten ist vielleicht doch eine zentrale Stelle für die Objekte eine speicherschonendere Strategie.
Ihr Kommentar
Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen.