pp.10.02-Logger

Logging in Multi-Threadumgebungen

  • Projekt: pp.10.02-Logger
  • Bearbeitungszeit: 35 Minuten
  • Musterlösung: 25 Minuten
  • Kompatibilität: mindestens Java SE 19

Im vorliegenden Anwendungsbeispiel soll einfacher Logger entwickelt werden, der Ausgaben auf der Konsole macht. Der Logger soll die nebenläufige Nutzung aus mehreren Threads heraus unterstützen.

Das Interface pp.Logging

Im Folgenden wird es vier alternative Varianten des Loggers geben, die unterschiedliche Strategien nutzen. In pp.Logging ist ein gemeinsames Interface spezifiziert, das von allen Varianten genutzt werden soll.

package pp;

import java.io.PrintStream;

public interface Logging {
    public enum Severity {
        Debug, Info, Notice, Warning, Error, Critical, Alert, Emergency;
    }

    void log(Severity level, String msg);

    void flush();

    void setPrintStream(PrintStream out);

    void setSeverityLevel(Severity level);

    Severity getSeverityLevel();

}

Unsichere Basisimplementierung SimpleLoggerUnsafe

SimpleLoggerUnsafe ist die nicht threadsichere Basisimplementierung des Logging-Interfaces:

public class SimpleLoggerUnsafe implements Logging, AutoCloseable {
    private static final int CAPACITY = 1024;
    private final StringBuilder log = new StringBuilder(CAPACITY);
    private Severity level;
    private PrintStream out;

    public SimpleLoggerUnsafe() {
        setSeverityLevel(Logging.Severity.Warning);
        setPrintStream(System.err);
    }

    public SimpleLoggerUnsafe(Severity level) {
        this();
        setSeverityLevel(level);
    }

    public SimpleLoggerUnsafe(Severity level, PrintStream out) {
        this(level);
        setPrintStream(out);
    }

    @Override
    public void log(Severity level, String msg) {
        if (this.level.ordinal() <= level.ordinal()) {
            if ((this.log.length() + msg.length() //
                    + System.lineSeparator().length()) >= log.capacity()) {
                flush();
            }
            this.log.append(msg).append(System.lineSeparator());
        }
    }

    @Override
    public void flush() {
        if (this.log.length() > 0) {
            this.out.print(this.log);
            this.out.flush();
            this.log.setLength(0);
        }
    }

    @Override
    public void setSeverityLevel(Severity level) {
        this.level = level;
    }

    @Override
    public void setPrintStream(PrintStream out) {
        this.out = out;
    }

    @Override
    public void close() throws Exception {
        flush();
    }

    @Override
    public Severity getSeverityLevel() {
        return this.level;
    }
}

Aufgabe 1: Gegenseitiger Ausschluss

Verwenden Sie für SimpleLoggerSafe die nicht threadsichere Basisimplementierung SimpleLoggerUnsafe und machen Sie sie threadsicher, indem Sie gegenseitigen Ausschluss umsetzen. Diese Lösung ist zwar schnell umgesetzt, aber nicht sehr effizient, da Threads, die “loggen” wollen, (blockierend) aufeinander warten müssen, obwohl sie möglicherweise wichtigere Anweisungen aus ihrer jeweiligen Hauptaufgabe erledigen sollten.

Aufgabe 2: Threadlokale SimpleLoggerUnsafe-Instanzen

Entwickeln Sie als Verbesserung eine weitere threadsichere alternative Implementierung des Interfaces Logging, bei der jeder Thread seine eigene Instanz des nicht threadsicheren SimpleLoggerUnsafe verwendet. Benutzen Sie dazu das folgende bereits vorgegebene Code-Gerüst:

public class ThreadLocalLogger implements Logging {
  private ThreadLocal<Logging> logger = ThreadLocal.withInitial(() -> {
    // hier programmieren und ein ThreadLocal<Logging> statt null
    // zurückgeben
    return null;
  });
  /* ... */
}

Ihre Aufgabe ist es, die Initialisierung von logger auszuprogrammieren. In der Klasse pp.ThreadLocalLogger sind die Implementierungen der Methoden des Interface Logging bereits enthalten: Diese Methoden delegieren Aufrufe an die jeweils threadlokal verpackte SimpleLoggerSafe-Instanz weiter, die mit logger.get() (s.o.) abgerufen wird - beispielweise wie in log():

@Override
public void log(Severity level, String msg) {
  logger.get().log(level, msg);
}

Das folgende Sequenzdiagramm zeigt beispielhaft die intendierte Nutzung der threadlokalen Implementierung des Logging-Service:

  • Aus einer Main-Klasse mit der main-Methode werden die zwei Threads t1 (blau) und t2 (grün) erzeugt.
  • Beide Threads haben in ihrer run-Methode eine Referenz auf die Klasse ThreadLocalLogger.
  • Da in ThreadLocalLogger die Instanzvariable logger mit einem Wrapper-Objekt vom Typ ThreadLocal<Logging> initialisiert wird, das wiederum ein SimpleLoggerUnsafe-Objekt beinhaltet, wird für jeden der beiden Threads t1 und t2 jeweils ein neues threadlokales SimpleLoggerUnsafe-Objekt bereitgestellt, auf das die Threads einheitlich über ThreadLocalLogger.log() zugreifen können.
  • Im dargestellten Beispiel ruft zuerst t1 die Methode log() an ThreadLocalLogger auf. Dies bewirkt, dass log() an dem für t1 threadlokalen Objekt logger (im blauen Bereich) aufgerufen wird.
  • Danach gibt es einen log()-Aufruf von t2.
  • Das nächste Beispiel ist davon unabhängig. Hier überlappen sich die beiden log()-Aufrufe der beiden Threads jedoch zufälligerweise. Ein gegenseitiger Ausschluss ist jedoch nicht erforderlich, da beide Threads nur jeweils an ihren threadlokalen SimpleLoggerUnsafe-Objekten log() aufrufen.

Aufgabe 3: Asynchrones Logging mit BlockingQueue-Instanzen

Für QueuedLogger als weiterer Implementierung des Interfaces Logging soll der Logger als eigenständiger Thread laufen. Verwenden Sie das Code-Gerüst von QueuedLogger im Package pp:

final class QueuedLogger extends Thread implements Logging {

    private final Logging logger;

    // ... TODO: BlockingQueues deklarieren ...

    public QueuedLogger() {
        logger = null; // ... TODO: initialisieren ...
        // ... TODO: BlockingQueues initialisieren ...
        setDaemon(true);
        setPriority(Thread.MIN_PRIORITY);
        start(); // hier kein Problem, da Klasse final (s. pp.01.01-Inheritance)
    }

    @Override
    public void run() {
        while (true) {
            // ... TODO: BlockingQueues auswerten und loggen ...
        }
    }

    @Override
    public void log(Severity level, String msg) {
        switch (level) {
        case Debug:
            // ... TODO: Blocking Queue befüllen ...
        case Info:
            // ... TODO: Blocking Queue befüllen ...
        case Notice:
            // ... TODO: Blocking Queue befüllen ...
        case Warning:
            // ... TODO: Blocking Queue befüllen ...
        case Error:
            // ... TODO: Blocking Queue befüllen ...
        case Critical:
            // ... TODO: Blocking Queue befüllen ...
        case Alert:
            // ... TODO: Blocking Queue befüllen ...
        case Emergency:
            // ... TODO: Blocking Queue befüllen ...
        }
    }

    @Override
    public void flush() {
        logger.flush();
    }

    @Override
    public void setSeverityLevel(Severity level) {
        logger.setSeverityLevel(level);
    }

    @Override
    public Severity getSeverityLevel() {
        return logger.getSeverityLevel();
    }

    @Override
    public void setPrintStream(PrintStream out) {
        logger.setPrintStream(out);
    }
}

Dieser Thread liest zu loggende Nachrichten aus unterschiedlichen BlockingQueue-Instanzen für die zu unterstützenden Prioritäten. Die Synchronisierung des Logging wird also im Go-Stil (“selektives Warten”) quasi wie durch die Nutzung von Kanälen realisiert. Das muss in der run-Methode umgesetzt werden.

Das Befüllen der BlockingQueue-Instanzen muss in der log-Methode implementiert werden.