Muster: Java: Proxy
Erläuterungen
BearbeitenDemonstriert werden soll ein synchronisierender Stellvertreter (engl. synchronizing proxy) anhand des folgenden Szenarios: Aus einem Lager (Interface Lager
) können Verbraucher eine bestimmt Anzahl von Gegenständen entnehmen (entnehme(int anzahl)
); das Entnehmen dezimiert natürlich den im Lager verbliebenen Vorrat (this.vorrat -= anzahl
). Nun haben wir es aber mit mehreren Verbrauchern zu tun, die gleichzeitig (als Threads implementiert) auf das Lager zugreifen wollen - race-conditions können die Folge sein; hier ein Beispiel:
vorrat <- 100 -- Thread von Verbraucher 1 -- entnehme(10) lokale variable: temp1 temp1 <- vorrat // temp1 = 100 temp1 <- temp1 - 10 // temp1 = 90 -- Unterbrechung durch Scheduler -- -----> -- Thread von Verbraucher 2 -- entnehme(20) lokale variable: tmp2 temp2 <- vorrat // temp2 = 100 temp2 <- temp2 - 20 // temp2 = 80 vorrat <- temp2 // vorrat = 80 -- Thread von Verbraucher 1 -- <----- -- Unterbrechung durch Scheduler -- vorrat <- temp1 // vorrat = 90
Bei diesem Beispielablauf wird der erste Thread unterbrochen bevor der verringerte Wert von 90 zurückgeschrieben wurde und stattdessen startet Thread 2 seine Berechnung (man beachte, dass es sich bei vorrat -= anzahl
nicht um eine atomare Operation handelt). Wenn Thread 1 schließlich wieder an der Reihe ist, fährt er genau an der Stelle fort, an der er zuvor unterbrochen wurde: Beim Zurückschreiben der temporären, lokalen Variable, in der natürlich immer noch der Wert 90 steht - als Endergebnis erhalten wir für vorrat
also den Wert 90, obwohl insgesamt 30 Einheiten entnommen wurden! Ein Vorgang, mit dem der Besitzer des Lagers vielleicht einverstanden sein könnte (immerhin konnte er 30 Einheiten in Rechnung stellen, musste aber nur 10 von seinem Vorrat abgeben), der aber nicht eben unserer Vorstellung von der Realität genügt...
Gelöst werden kann dieses Problem nur, indem der Zugriff auf die entnehme()
-Methode synchronisiert wird: Es muss sichergestellt sein, dass eine Entnahme-Operation vollständig abgeschlossen ist, bevor mit der nächsten begonnen wird. Die Synchronisation könnte zwar in der EchtesLager
-Klasse selbst stattfinden, wir wollen hier jedoch einen anderen Weg gehen und den Zugriff durch einen vorgelagerten Stellvertreter (SynchronisiertesLager
) reglementieren: Dieser Stellvertreter reicht Anforderungen unverändert an das echte Lager weiter, sorgt aber dafür, dass sich immer nur ein Thread innerhalb der entnehme()
-Methode des echten Lagers befindet (er tut dies durch die Anweisung synchronized(this.lager)
).
Da das echte Lager und sein synchronisierender Stellvertreter eine gemeinsame Schnittstelle aufweisen, bekommen Verbraucher von diesem Vorgang nichts mit: Ob sie direkt auf das echte Lager zugreifen, oder indirekt über einen Stellvertreter - es macht für sie keinen Unterschied.
Code
Bearbeiten interface Lager {
public void entnehme(int anzahl);
}
class EchtesLager implements Lager {
public int vorrat;
public void entnehme(int anzahl) {
this.vorrat -= anzahl;
}
}
/**
* Synchronisierender Stellvertreter
*/
class SynchronisiertesLager implements Lager {
Lager lager;
public SynchronisiertesLager(Lager lager) {
this.lager = lager;
}
public void entnehme(int anzahl) {
synchronized(this.lager) { // erlaube immer nur einem Thread den Zugriff auf das tatsächliche Lager
this.lager.entnehme(anzahl);
}
}
}
class Verbraucher extends Thread {
int wieoft, anzahl;
Lager lager;
/**
* Ein Verbraucher entnimmt <wieoft>-mal jeweils <anzahl> Elemente aus dem Lager
*/
public Verbraucher(int wieoft, int anzahl, Lager lager) {
this.wieoft = wieoft;
this.anzahl = anzahl;
this.lager = lager;
}
public void run() {
for (int i = 0; i < this.wieoft; i++) {
this.lager.entnehme(anzahl);
this.yield();
}
}
}
public class StellvertreterTest {
public static void main(String[] args) {
int maxWieoft = 1000;
int maxAnzahl = 80;
int anzahlVerbraucher = 100;
Verbraucher[] verbraucher = new Verbraucher[anzahlVerbraucher];
int anzahlGesamt = 0;
EchtesLager echtesLager = new EchtesLager();
SynchronisiertesLager synchronisiertesLager = new SynchronisiertesLager(echtesLager);
java.util.Random rand = new java.util.Random();
for (int i = 0; i < anzahlVerbraucher; i++) {
int wieoft = rand.nextInt(maxWieoft + 1);
int anzahl = rand.nextInt(maxAnzahl + 1);
anzahlGesamt += wieoft*anzahl;
verbraucher[i] = new Verbraucher(wieoft, anzahl, echtesLager); // hier passiert es, dass am Ende das Lager nicht ganz leer ist
// verbraucher[i] = new Verbraucher(wieoft, anzahl, synchronisiertesLager); // so wird das Lager immer vollständig geleert.
}
echtesLager.vorrat = anzahlGesamt;
for (int i = 0; i < anzahlVerbraucher; i++) {
verbraucher[i].start();
}
for (int i = 0; i < anzahlVerbraucher; i++) {
try {
verbraucher[i].join();
} catch (InterruptedException e) { }
}
System.out.println(echtesLager.vorrat);
}
}