Advanced   Java   Services ThreadLocal


Ein Objekt, zwei Threads (in der JVM)

Hat man ein Javaobjekt vom Typ Runnable und startet man den zugehörigen Thread, so laufen run() und der Konstruktor des Objekts in verschiedenen Threads der JVM. Das folgende Beispiel zeigt dies.

 /**
  * Zeigt, daß der Konstruktor und run() in verschiedenen Threads laufen
  */
 private static void beispiel_0()
{
  Runnable runner = new Runnable()
          {
          {
            System.out.println("konstruktor: " + Thread.currentThread().getName());
          }
            @Override
            public void run()
            {
              System.out.println("run:         " + Thread.currentThread().getName());
            }} ;


    Thread thread = new Thread(runner);
    thread.start();
}

Die Ausgabe

konstruktor: main
run:         Thread-0

Zwei Objekte gleichen Typs

Zwei Objekte einer Klasse Task, die Runnable implementiert. Objekt 1 erhält über den Konstruktor eine Referenz auf Objekt 2. Dann kann Objekt 1 auf den privaten Datenteil von Objekt 2 zugreifen. Diese Eigenschaft wird hier verwendet um in run() von Objekt 1 das Objekt 2 zu manipulieren.

Die Klasse Task

class Task implements Runnable
{
  private Task another;
  private String instanceData = Task.class.getName();

  public Task(Task another)
  {
    this.another = another;
  }

   public String getData()
   {
      return instanceData;
   }

   public void setData(String data)
   {
      instanceData = data;
   }
   @Override
  public void run()
  {
    this.instanceData = Thread.currentThread().getName();
    if (another != null)
    {
      another.instanceData = Thread.currentThread().getName();
      // Zugriff auf private Daten
    }
  }
}

Falls in run() das fremde Objekt gleichen Typs verfügbar ist, werden die Daten manipuliert.

   private static void beispiel_1()
  {
    Task t0 = new Task(null);
    Task t1 = new Task(t0);
    Thread th0 = new Thread(t0);
    th0.start();
    try{ th0.join(); } catch(InterruptedException ex){};
    System.out.println("t0.getData() = " + t0.getData());

    Thread th1 = new Thread(t1);
    th1.start();
    try{ th1.join(); } catch(InterruptedException ex){};
    System.out.println("t1.getData() = " + t1.getData());
  }

Die Ausgabe

t0.getData() = Thread-0
t0.getData() = Thread-1

Da das Javaobjekt auch nach dem Ende des Threads noch lebt, kann der zweite Thread das Objekt t0 manipulieren.







Auf welche Daten hat ein Thread Zugriff

run() kann natürlich lokale Variablen verwenden und hat Zugriff auf alle Instanzvariablen und statische Variablen der Klasse in der sich run() befindet. Da an run() keine Parameter übergeben werden können, müssen externe Daten über den Konstruktor oder eine set-Methode in den (privaten) Datenteil des Threadobjekts gebracht werden. Das nächste Beispiel zeigt diesen Vorgang an zwei Runnableobjekten.


Zwei Threads manipulieren ihren zugehörigen Datenteil im Runnableobjekt

Die Daten werden zunächst über den Konstruktor initialisiert und dann in run() durch den Thread verändert. Die Runnable implementierende Klasse. Es gibt einen statischen und einen instanzbezogenen Datenteil

class Runner implements Runnable
{
   private static StringBuilder staticData = new StringBuilder("staticData");
   private StringBuilder instanceData = new StringBuilder();

   public Runner(String data)
   {
      this.instanceData.append(data);
   }

   public String getData()
   {
      return instanceData.toString();
   }

   public static String getStaticData()
   {
      return staticData.toString();
   }

   @Override
   public void run()
   {
      instanceData.append(" " + Thread.currentThread().getName());
      staticData.append(" " + Thread.currentThread().getName());
   }
}

Die Mainklasse

  private static void beispiel_2()
  {
    Runner runner0 = new Runner("instance-0");
    Runner runner1 = new Runner("instance-1");
    System.out.println(runner0.getData());
    System.out.println(runner1.getData());
    System.out.println();
    Thread thread0 = new Thread(runner0);
    Thread thread1 = new Thread(runner1);
    thread0.start();
    thread1.start();

    try
    {
       thread0.join(10);
       thread1.join(10);  // main waits for both
    }
    catch(InterruptedException ex)
    {
       // TODO Auto-generated catch block
       ex.printStackTrace();
    }

    System.out.println(runner0.getData());
    System.out.println(runner1.getData());
    System.out.println(Runner.getStaticData());
  }

Die Ausgabe ist meist

instance-0
instance-1

instance-0 Thread-0
instance-1 Thread-1
staticData Thread-0 Thread-1

in seltenen Fällen auch so

instance-0
instance-1

instance-0 Thread-0
instance-1 Thread-1
staticData Thread-1 Thread-0

Jeder Thread greift auf die instanzbezogenen Daten "seines" Runnerobjekts zu, die statischen Daten werden gemeinsam verwendet.


Zwei Threads manipulieren die Daten eines (externen) Objekts

Mit dem gleichen Trick kann man auch mehreren Threads den Zugriff auf ein und dasselbe Objekt ermöglichen. Auch in diesem Fall kännen wir nicht mit Strings arbeiten, da Strings immer als Konstanten angelegt werden. Wir nehmen wieder StringBuilder. Die Runnable implementierende Klasse sieht nun folgendermaßen aus.

class Runner implements Runnable
{
   private StringBuilder data;

   public Runner(StringBuilder data)
   {
      this.data = data;
   }

   @Override
   public void run()
   {
      data.append(" " + Thread.currentThread().getName());
   }
}

Die Mainklasse

public class Beispiel_3
{
   public static void main(String[] args)
   {
      StringBuilder sharedData = new StringBuilder("Wer schreibt zuerst :");
      Runner runner1 = new Runner(sharedData);
      Runner runner2 = new Runner(sharedData);

      Thread thread1 = new Thread(runner1);
      Thread thread2 = new Thread(runner2);
      thread1.start();
      thread2.start();

      try
      {
         thread1.join();
         thread2.join();  // main waits for both
      }
      catch(InterruptedException ex)
      {
         ex.printStackTrace();
      }
      System.out.println(sharedData);
   }
}

Die Ausgabe sieht meistens so aus.

Wer schreibt zuerst : Thread-0 Thread-1

Kann aber gelegentlich auch so aussehen.

Wer schreibt zuerst : Thread-1 Thread-0

Auch wenn im Code die start()-Methode zu thread1 zuerst steht, ist das keine Gewähr dafür, daß dieser Thread wirklich als erster startet.







ThreadLocal

Die in Java 1.2 eingeführte Klasse ThreadLocal ermöglicht eine weitere Variante des Datenzugriffs. Ein ThreadLocalobjekt verwaltet einen Datenpool von Instanzen gleichen Typs, von denen jede genau einem Thread zugeordnet ist. Hierzu das folgende Beispiel.


Ein erstes Beispiel

Wir legen ein Objekt vom Typ ThreadLocal mit dem generischen Typ String in main() an und geben dieses Objekt an zwei Threads weiter. Jeder Thread schreibt mit Hilfe der set()-Methode seinen Namen in das Objekt und gibt die geschriebenen Daten zur Kontrolle auf die Konsole aus. Am Schluß gibt main() die Daten aus.

Die Threadklasse

public class Task extends Thread
{
   private ThreadLocal<String> threadLocalString;

   public Task(String name, ThreadLocal<String> threadLocalString)
   {
      this.threadLocalString = threadLocalString;
      this.setName(name);
   }

   @Override
   public void run()
   {
      threadLocalString.set(Thread.currentThread().getName());
      System.out.println(threadLocalString.get());
   }
}

Die Mainklasse

public class ThreadLocal01
{
   public static void main(String args[])
   {
      ThreadLocal<String>  threadLocalString = new ThreadLocal<>();
      Task threadOne = new Task("Task 1",threadLocalString);
      Task threadTwo = new Task("Task 2",threadLocalString);
      threadOne.start();
      threadTwo.start();

      try
      {
         threadOne.join();
         threadTwo.join();
      }
      catch(InterruptedException ex)
      {
         ex.printStackTrace();
      }
      System.out.println(threadLocalString.get());
   }
}

Die Ausgabe schaut meist so aus

Task 1
Task 2
null

gelegentlich aber auch so

Task 2
Task 1
null

Erklärung

Greift ein Thread zum Erstenmal mit set() auf ein ThreadLocalobjekt zu wird er registriert und der an set() übergebene Wert wird in eine Variable geschrieben, die nur für diesen Thread zuständig ist und solange existiert, solange der Thread existiert. Auf diese Weise erhält jeder Thread der auf dieses ThreadLocalobjekt Zugriff sein eigenes Object vom vorher vereinbarten generischen Typ. In vorigen Beispiel verwaltet das ThreadLocalobjekt zwei Strings, einen für Task 1, einen für Task 2. Mit dem letzten Statement in main greift der MainThread zum erstenmal auf das ThreadLocalobjekt zu. Da er aber keinen Wert setzt, liefert get() null zurück.

Die Klasse ThreadLocal wurde von Joshua Bloch und Doug Lea für die Javaversion 1.2 entworfen und arbeitet intern mit einer HashMap die mit Weakreferences arbeitet. Im wesentlichen ist dies eine Key-Value Tabelle bei der die Keys die Thread-ID's sind und die Values die dazugehörigen Daten.


Anwendung 1

Wenn man ein beliebiges Objekt einem ThreadLocalobjekt übergibt so wird dieses automatisch threadsafe, denn jeder Thread, der darauf zugreift erhält einmalig eine Kopie, die nur er bearbeiten darf. Kein anderer Thread hat darauf Zugriff. Das klassische Beispiel im Netz ist die Klasse SimpleDateFormat, die nicht threadsafe ist aber auf diese Weise threadsafe gemacht werden kann. Allerdings ist dieses Beispiel mit der neuen Java 8 Time API obsolet, da die Nachfolgeklasse DateTimeFormatter threadsafe ist.


Anwendung 2

Mit ThreadLocal kann man sich Übergabeparameter an Methoden ersparen. Dies zeigt das nächste Beispiel. das beispiel arbeitet mit einer Singletonklasse, die ein generisches ThreadLocalobjekt liefert . Dabei wird auch die neue Java 8 Methode von ThreadLocal eingesetzt.


Beispiel 2: Eine generische Singleton ThreadFactory

Die Singletonklasse

/**
 * Singletonpattern
 * Die getInstance()-Methode der Factory hat die gleiche Signatur wie die statische
 * 1.8-Methode von ThreadLocal. Damit kann man dem ThreadLocalobjekt über einen
 * Supplier einen Initialwert übergeben. Will man keinen Initialwert haben so kann
 * man 'null' übergeben. 'null' darf aber nicht an withInitial() weitergereicht werden
 * weil ein null_Suppier eine NPE erzeugt. In diesem Fall verwendet man einen Supplier,
 * der einen leeren StrinBuilder erzeugt.
 */
class ThreadLocalFactory
{
  static ThreadLocal instance = null;

  private ThreadLocalFactory()
  {}

  public static <V> ThreadLocal<V> getInstance(Supplier<? extends V> initialValue)
  {
    if(instance == null)
    {
      if (initialValue == null)
        return (instance =  ThreadLocal.withInitial( StringBuilder::new ) );  // Java 1.8 Methodenreferenz
        //return (instance =  ThreadLocal.withInitial( () -> new StringBuilder()) );  // Java 1.8 Lambdaausdruck
        // Supplier liefert einen leeren StringBuilder

        return (instance =  ThreadLocal.withInitial(initialValue));  // 1.8
    }
    return instance;
  }
}

Eine Runnable, das sich ein Threadlocal über die Factoryklasse holt und Daten schreibt. Man beachte, daß die in run() verwendeten Methoden keine Übergabeparameter haben.

class Runner implements Runnable
{
  @Override
  public void run()
  {
    ThreadLocal<StringBuilder> tlf = ThreadLocalFactory.getInstance(null);
    try
    {
      tlf.get().append( Thread.currentThread().getName()+ " : X") ;
      doSomething1();
      doSomething2();
      System.out.println(tlf.get());
    }
    finally
    {
      tlf.remove();  // Java 1.5 Methode
    }
  }

  private void doSomething1()
  {
    ThreadLocal<StringBuilder> threadLocal = ThreadLocalFactory.getInstance(null);
    threadLocal.get().append(", Y");
  }

  private void doSomething2()
  {
    ThreadLocal<StringBuilder> threadLocal = ThreadLocalFactory.getInstance(null);
    threadLocal.get().append(", Z");
  }
}

Die Mainklasse

class Beispiel2
{
  public static void main(String args[])
  {
    // kein anfangswert
    //ThreadLocal<StringBuilder> threadLocal = ThreadLocalFactory.getInstance(null);
    ThreadLocal<StringBuilder> threadLocal =
        //ThreadLocalFactory.getInstance(null);
        ThreadLocalFactory.getInstance(() -> new StringBuilder("initialValue "));

    System.out.println(threadLocal.get());

    ThreadLocal<StringBuilder> threadLocal2 = ThreadLocalFactory.getInstance(null);
    System.out.println(threadLocal2.get());
    System.out.println(threadLocal.get());

//  System.out.println( threadLocal.equals(threadLocal2));  // true
//  System.out.println( threadLocal == threadLocal2 );  // true
    System.out.println();

    threadLocal.get().append("main     : A");
    threadLocal.get().append(", B");
    threadLocal.get().append(", C");
    System.out.println(threadLocal.get());

    Runner runner = new Runner();
    Thread thread = new Thread(runner);
    Thread thread2 = new Thread(runner);
    thread.start();
    thread2.start();

  }  // end main
}  // end main class

Die Ausgabe

initialValue
initialValue
initialValue

initialValue main     : A, B, C
initialValue Thread-0 : X, Y, Z
initialValue Thread-1 : X, Y, Z