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(
…)
:
Das folgende Sequenzdiagramm zeigt beispielhaft die intendierte Nutzung der threadlokalen Implementierung des Logging-Service:
- Aus einer
Main
-Klasse mit dermain
-Methode werden die zwei Threadst1
(blau) undt2
(grün) erzeugt. - Beide Threads haben in ihrer
run
-Methode eine Referenz auf die KlasseThreadLocalLogger
. - Da in
ThreadLocalLogger
die Instanzvariablelogger
mit einem Wrapper-Objekt vom TypThreadLocal<Logging>
initialisiert wird, das wiederum einSimpleLoggerUnsafe
-Objekt beinhaltet, wird für jeden der beiden Threadst1
undt2
jeweils ein neues threadlokalesSimpleLoggerUnsafe
-Objekt bereitgestellt, auf das die Threads einheitlich überThreadLocalLogger.log(
…)
zugreifen können. - Im dargestellten Beispiel ruft zuerst
t1
die Methodelog(
…)
anThreadLocalLogger
auf. Dies bewirkt, dasslog(
…)
an dem fürt1
threadlokalen Objektlogger
(im blauen Bereich) aufgerufen wird. - Danach gibt es einen
log(
…)
-Aufruf vont2
. - 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 threadlokalenSimpleLoggerUnsafe
-Objektenlog(
…)
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.