Threadsicherheit (Threadsafety) ist ein häufig unterschätztes Thema bei Entwicklern. Dabei reichen einige wenige Heuristiken, um das Schlimmste zu vermeiden.

Das Problem

Die Threadsicherheit ist üblicherweise dann gefährdet, wenn mehrere Prozesse auf eine gemeinsame Ressource zugreifen und sich der Zustand dieser Ressource dabei ändert.

Keine Änderung

Ein Objekt, das sich nicht ändert, ist per se threadsicher. Aus diesem Vorteil (und einigen weiteren Vorteilen) haben sich die Paradigmen der unveränderlichen Objekte (immutable objects) und der zustandslosen Dienste (stateless services) entwickelt.

Wenn in einer Klasse also nichts verändert werden kann, dann ist sie Threadsicher.
Beispiel hierfür wäre ein Comparator, der zwei Strings ohne Beachtung der Groß- und Kleinschreibung vergleicht:

public class CaseInsentiveComparator implements Comparator<String> {
    @Override
    public int compare(String o1, String o2) {
        return o1.toLowerCase().compareToIgnoreCase(o2.toLowerCase());
    }
}

Da hier kein Zustand für den Comparator hinterlegt, geschweige denn geändert wird, kann theoretisch eine Instanz für alle Verwendungen eingesetzt werden und das ganze ist stets Threadsicher:

class CaseInsentiveComparator implements Comparator<String> {
    public static final CaseInsentiveComparator INSTANCE = new CaseInsentiveComparator();
 
    @Override
    public int compare(String o1, String o2) {
        return o1.toLowerCase().compareToIgnoreCase(o2.toLowerCase());
    }
}
 
...
CaseInsentiveComparator.INSTANCE.compare("hallo", "Hallo");

Was tun bei Änderungen

Anders sieht es aus, wenn ein Zustand involviert ist. Angenommen, wir verwenden eine Collator-Instanz, um unsere Strings mit Berücksichtigung von deutschen Umlauten zu sortieren. Hierzu erhält unser Comparator eine Collator-Instanz:

class GermanStringComparator implements Comparator<String> {
    private final Collator collator = Collator.getInstance(Locale.GERMAN);
 
    @Override
    public int compare(String o1, String o2) {
        return collator.compare(o1, o2);
    }
}

Ist dieser Comparator nun threadsicher, obwohl nur lesend zugegriffen wird?

Das hängt davon ab, ob Collator selber threadsicher ist. Eine kurze Suche ergibt den Hinweis, dass es zumindest in einigen JDK-Versionen der Collator nicht threadsafe ist. Offensichtlich verwendet der Collator intern Zustände, die sich bei Verwendung ändern können.

Daher sollte man unseren Comparator entweder pro Anwendungsfall und Prozess instantiieren, oder ihn threadsicher machen.
In diesem Fall könnte man entweder bei jeder Benutzung eine Collator-Instanz erzeugen, eine Synchronisierung auf den Collator oder auf die Methode des Comparators machen:

Immer neu

Bei jeder Benutzung eine Collator-Instanz erzeugen:

class GermanStringComparator implements Comparator<String> {
 
    @Override
    public int compare(String o1, String o2) {
        final Collator collator = Collator.getInstance(Locale.GERMAN);
        return collator.compare(o1, o2);
    }
}

Diese Variante ist immer sicher, kann aber langsam und ineffizient sein, wenn das erzeugte Objekt komplex ist und viel Speicher oder andere Ressourcen benötigt.

Synchronisierung

Synchronisierung auf den Collator:

class GermanStringComparator implements Comparator<String> {
    private final Collator collator = Collator.getInstance(Locale.GERMAN);
 
    @Override
    public int compare(String o1, String o2) {
        synchronized(collator){
            return collator.compare(o1, o2);
        }
    }
}

Synchronisierung auf die Methode des Comparators:

class GermanStringComparator implements Comparator<String> {
    private final Collator collator = Collator.getInstance(Locale.GERMAN);
 
    @Override
    public synchonized int compare(String o1, String o2) {
        return collator.compare(o1, o2);
    }
}

Bei der Synchronisierung wird nur einem Prozess erlaubt, den synchronisierten Bereich gleichzeitig zu betreten. Das hat den Vorteil, dass man nicht ständig neue Objekte erzeugen muss, aber auch den Nachteil, dass mehrere Prozesse nicht gleichzeitig arbeiten können, eine Parallelisierung also ausgebremst wird.

Prozessgebundene Instanzen

In Java besteht die Möglichkeit, eine Objekt-Instanz mit einem Thread / Prozess zu verbinden, so dass dieser nur von dem entsprechenden Prozess verwendet werden kann. Hierzu existiert das ThreadLocal. Da die Instanz nur von einem Prozess verwendet wird, ist sie threadsicher.

class GermanStringComparator implements Comparator<String> {
    private static final ThreadLocal<Collator> COLLATOR_THREAD_LOCAL = new ThreadLocal() {
        @Override
        protected Collator initialValue() {
            return Collator.getInstance(Locale.GERMAN);
        }
    };
 
    public static final GermanStringComparator INSTANCE = new GermanStringComparator();
 
    @Override
    public int compare(String o1, String o2) {
        return COLLATOR_THREAD_LOCAL.get().compare(o1, o2);
    }
}

Diese Variante hat den Vorteil, dass sie die Parallelisierung nicht abbremst und auch nur so viele Instanzen des Collators erzeugt, wie Threads existieren. Unter der Annahme, dass Threads wiederverwendet werden, und nicht ständig neu erzeugt, ist das recht effizient.

Weitere Beispiele

Weitere Beispiele für Klassen, die nicht threadsicher sind, sind Matcher (Pattern) oder
SimpleDateFormatter / DateFormatter.

Ein Klassiker ist es auch, einen threadsicheren Service zu schreiben, und dann durch neue Anforderungen um Zustände zu ergänzen, ohne diese abzusichern.
So könnte z. B. ein Service, der Daten aus der Datenbank lädt, um das Nachhalten von einigen Statistiken erweitert werden:

@Service
class LoadService  {
    @Autowired
    private MyClassDao dao;
 
    public MyClass loadByName(String name) {
        return dao.loadByName(name);
    } 
}

Nun “naive” Erweiterung um Statistik:

@Service
class LoadService  {
    @Autowired
    private MyClassDao dao;
 
    private int accessCounter = 0;
 
    public MyClass loadByName(String name) {
        accessCounter++;
        return dao.loadByName(name);
    }
}

Das dieser Service nun nicht mehr threadsicher ist, sollte klar sein. Der Zugriff auf die Variable accessCounter muss nun abgesichert werden.

Zusammenfassung

Es gilt also generell in Erfahrung zu bringen, ob eine Klasse threadsicher ist oder nicht.

Wenn man weiß, dass eine Klasse threadsicher ist, dann kann man eine einzelne Instanz verwenden.
Wenn man nicht weiß, ob eine Klasse threadsicher ist, sollte man sie als nicht-threadsicher behandeln.
Nicht threadsichere Klassen sollte man mit einem der verschiedenen Möglichkeiten threadsicher machen.

Referenzen & weiterführende Links