Wenn Sie eine Applikation programmiert haben und diese produktiv einsetzten möchten, ist es oftmals nötig (aus welchen Gründen auch immer), dass dieses Programm nur ein einziges mal ausgeführt wird. Wenn in Java ein Programm allerdings zwei mal ausgeführt wird, starten beide Programme in einer unterschiedlichen Virtual Machine, weshalb mit gewöhnlichen Methoden nicht festgestellt werden kann, ob die Anwendung bereits läuft oder nicht. Dieses Kapitel beschreibt Ihnen einige Lösungsansätze für dieses Problem. Unter Umständen werden Sie die Lösungsverfahren (Firewall-Einstellungen, beschränkte Schreibrechte, …) im praktischen Einsatz aber noch auf Ihre individuellen Bedürfnisse anpassen müssen.
Eine Lock-Datei
Eine Möglichkeit wäre es, dass Ihr Programm beim Starten in einem Verzeichnis eine Datei anlegt. Beim Beenden des Programms wird diese Datei dann wieder gelöscht (siehe Code beim Beenden ausführen). Wird jetzt das Programm ein weiteres Mal gestartet, erkennt es (anhand der bereits vorhandenen Lock-Datei), dass das Programm bereits ausgeführt wird und kann darauf reagieren, indem sich die zweite Instanz selbst beendet.
package de.jbb.startonce;
import java.io.File;
import java.io.IOException;
public class FileLock {
public static void main(String[] args) throws IOException {
final File f = new File(System.getProperty("java.io.tmpdir") + "/FileLock.lock");
if (f.exists()) {
System.out.println("Wird schon ausgeführt ... Bye Bye");
System.exit(0);
}
f.createNewFile();
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
public void run() {
f.delete();
}
}));
System.out.println("Erster!");
try {
Thread.sleep(10000);
}
catch (InterruptedException ie) {}
System.out.println("Beendet");
}
}
Um diese Methode zu testen, führen Sie das Programm einfach zweimal kurz hintereinander aus.
Die Vorteile liegen auf der Hand: Diese Methode ist sehr einfach zu implementieren und sollte auch fast überall (unabhängig von Randbedingungen wie z. B. Firewall-Einstellungen) laufen. Dafür fallen die Nachteile aber umso gravierender aus. Sollte Ihr Programm die Datei beim Beenden nicht mehr löschen können (Absturz der VM, Absturz des Betriebssystems, Stromausfall, …) kann Ihre Anwendung nicht mehr gestartet werden, bis die Lock-Datei manuell entfernt wurde. Auch ist keine Kommunikation zwischen den gestarteten Applikationen möglich.
Von dieser Methode ist also abzuraten.
Einen Port sperren
Eine weitere Möglichkeit wäre es einen Port über einen ServerSocket zu sperren. Versucht eine weitere Anwendung den Port für sich zu beanspruchen, wird eine Exception geworfen und die zweite Applikation weiß, dass das Programm bereits gestartet wurde. Dies hat den Vorteil gegenüber der vorhergehenden Methode, dass auch bei einem Absturz der Port wieder freigegeben wird. Die Anwendung kann also nicht versehentlich “für immer” gesperrt werden. Eine mögliche Implementierung sieht so aus:
package de.jbb.startonce;
import java.io.IOException;
import java.net.ServerSocket;
public class SocketLock {
public static void main(String[] args) {
try {
new ServerSocket(7238);
}
catch (IOException e) {
System.out.println("Programm läuft schon");
System.exit(0);
}
System.out.println("Ich bin der Erste!");
try {
Thread.sleep(10000);
}
catch (InterruptedException ie) {}
System.out.println("Und tschüss!");
}
}
Zum Testen sollten Sie auch hier das Programm öfter kurz hintereinander ausführen.
Der Vorteil dieser Methode liegt (wie bereits erwähnt) ganz klar darin, dass es nicht passieren kann, dass sich Ihre Anwendung wegen eines Fehlers nur noch durch manuelles Eingreifen des Users starten lässt. Allerdings bleibt das Problem, dass die beiden Anwendungen auch hier nicht miteinander kommunizieren können – noch nicht! Viel schwerwiegender ist aber, dass sich zwei neue Probleme auftun. Was ist, wenn die lokale Firewall diesen Vorgang unterbindet oder zumindest (durch Warten auf eine User-Interaktion) stark verlangsamt? Oder eine andere Anwendung diesen Port bereits belegt?
Diese Methode ist also auch nicht optimal, meiner Meinung nach aber empfehlenswerter als unsere erste Idee.
Kommunikation zwischen beiden Anwendungen
Erweitern wir unsere Klasse SocketLock noch dahingehend, dass wir nicht einfach nur einen ServerSocket erstellen, sondern diesen auch an dessen Port lauschen, und falls der Port schon besetzt ist, eine Verbindung zu diesem Port aufbauen lassen. Damit ermöglichen wir einen Informationsaustausch zwischen den beiden Programmen. Dies ist z. B. dann nützlich, wenn der zweite Aufruf des Programms andere Parameter enthält als es der Erste tat, und das laufende Programm diese Parameter verarbeiten soll.
package de.jbb.startonce;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
public class SocketLock {
public static void main(String[] args) {
try {
final ServerSocket ss = new ServerSocket(7238);
new Thread(new Runnable() {
public void run() {
while (true) {
try {
Socket s = ss.accept();
BufferedReader buffy = new BufferedReader(new InputStreamReader(s.getInputStream()));
System.out.println("Zweite Anwendung sagt: " + buffy.readLine());
buffy.close();
s.close();
}
catch (IOException e) {
System.out.println("Verbindung beendet");
}
}
}
}).start();
}
catch (IOException e) {
try {
Socket s = new Socket("localhost", 7238);
s.getOutputStream().write("Oha, da bin ich wohl zu spät. Sorry!\n".getBytes());
s.close();
}
catch (UnknownHostException e1) {
e1.printStackTrace();
}
catch (IOException e1) {
e1.printStackTrace();
}
System.exit(0);
}
}
}
Beachten Sie, dass sich das zuerst gestartete Programm nun nicht mehr selbst nach 10 Sekunden beendet, sondern manuell durch Sie abgebrochen werden muss (schließen der Konsole).
Dieses Methode löst zwar das Problem, dass die Anwendungen nicht miteinander kommunizieren können, unsere anderen Nachteile werden aber noch nicht beachtet. Zudem erhöht sich die Komplexität dieser Variante merklich.
Die Kombination macht’s
Beim Betrachten der Probleme der FileLock, und der SocketLock-Klasse fällt auf, dass beide Varianten andere Probleme haben. Daraus lässt sich schließen, dass beide Verfahren irgendwie miteinander verknüpft werden sollten, um eine optimale Lösung zu erzielen.
Nehmen wir die Klasse SocketLock als Basis. Um sicherzustellen, dass keine andere Applikation den gewählten Port verwendet, sollte der Port dynamisch ausgewählt werden. Z. B. so:
private int getFreeServerSocket() {
for (int i = 2000; i < 10000; i++) {
try {
new ServerSocket(i);
return i;
}
catch (IOException ignore) {}
}
return -1;
}
Natürlich stellt sich nun die Frage, wie dann andere Instanzen unserer Applikation den richtigen Port finden. Der Port muss also irgendwo hinterlegt werden. Hierzu bietet sich unsere Methode aus der Klasse FileLock an. Wir erstellen in einem beliebigen Verzeichnis eine neue Datei. Diese Datei enthält den gewählten Port. Das hat folgende Vorteile:
- Ist die Datei nicht vorhanden, läuft die Applikation auf keinen Fall schon einmal.
- Ist einmal ein Port gefunden, muss er meistens nicht jedes Mal neu gewählt werden.
- Da der Lock nicht über die Datei, sondern über den Port geschieht, muss (bzw. soll) die Datei mit dem Port beim Beenden des Programms nicht gelöscht werden.
- Kommunikation zwischen den einzelnen Instanzen ist über den gewählten Port möglich.
- Kann keine Verbindung zum Port in der Datei aufgebaut werden, wurde die Anwendung noch nicht gestartet.
Doch was passiert, wenn eine andere Anwendung eine Verbindung zu dem Port aufbaut, an dem Ihre Anwendung bereits lauscht? Oder eine Anwendung sich “Ihren” Port geschnappt hat und jetzt an selbigen horcht? In diesem Fall können die von Ihnen vorgeschriebenen Kommunikationsregeln (z. B. ein Austausch ausschließlich über Strings) gebrochen, und Ihr Programm im schlimmsten Fall zum Absturz gebracht werden. Deshalb sollten Sie bei jeder neuen Verbindung zu Ihrer laufenden Applikation eine Verifizierung durchführen. Dies dient auch zur Sicherstellung, dass es sich auch wirklich um Ihr Programm handelt, welches an diesem Port gebunden ist.
Eine solche Verifizierung kann bspw. über eine eigene Klasse, die einen eindeutigen Namen für Ihr Programm enthält, realisiert werden.
package de.jbb.sic;
import java.io.Serializable;
public class ClassCheck implements Serializable {
private static final long serialVersionUID = 1L;
private String className = null;
public ClassCheck(String className) {
setClassName(className);
}
public ClassCheck() {}
public String toString() {
return this.className;
}
public String getClassName() {
return this.className;
}
public void setClassName(String className) {
this.className = className;
}
}
Diese Klasse sollte dann über einen ObjektOutputStream an die bereits laufende Applikation gesendet, über einen ObjektInputStream von der laufenden Anwendung gelesen, und entsprechend verarbeitet werden.
Ein standardisiertes Verfahren
Auf diesem Wissen lässt sich nun eine kleine Bibliothek aufbauen, die dieses Verfahren standardisiert und sich somit immer wieder in Ihre Anwendungen integrieren lässt. Hierzu benötigen Sie drei Komponenten:
- Eine Klasse zu Verifizierung.
- Ein Interface als Schnittstelle für Ihre Programme.
- Eine Klasse, die die Logik implementiert.
Die Klasse zur Verifizierung kennen Sie bereits aus dem letzten Abschnitt – ClassCheck. Sehen wir uns als nächstes das Interface an. Es stellt drei Methoden bereit. Eine wird aufgerufen, falls das Programm noch mal gestartet wurde. Eine, falls sich ein anderes Programm zu diesem Port verbunden hat. Und eine letzte, falls eine andere Instanz Ihrer Applikation eine Nachricht an die bereits laufende Applikation geschickt hat.
package de.jbb.sic;
public interface ApplicationStartedListener {
/**
* Wird aufgerufen, wenn eine identische Anwendung gestartet wird, und sich auf den
* Server connecten will.
*/
void applicationStarted();
/**
* Wird aufgerufen, wenn eine andere Anwendung gestartet wurde, die sich dennoch
* auf den selben Port verbinden wollte, auf welchem auch der Server laeuft.
*
* @param name Name der Anwendung
*/
void foreignApplicationStarted(String name);
/**
* Wird aufgerufen, wenn eine später gestartete, identische Anwendung eine Nachricht
* schickt.
*
* @param obj die Nachricht als Object
*/
void messageArrived(Object obj);
}
Auf der nächsten Seite finden Sie die Implementierung der Logik.


{ 10 } Comments
Eine kleine Anmerkung zur FileLock-Methode: Seit Java NIO gibt es die Möglichkeit, “echte” File-Locks zu realisieren. Hierzu wird die Methode FileChannel#tryLock verwendet.
Dadurch fallen ein Großteil der Nachteile weg. Eine Kommunikation ist allerdings immer noch nicht möglich. Solch ein FileLock wird aber eben automatisch aufgehoben sobald das Programm nicht mehr läuft. Das wichtige Kriterium wäre nicht mehr, ob die Datei existiert, sondern ob tryLock ein FileLock oder null zurückgibt. Ob die Datei noch existiert, kann dafür als Kriterium verwendet werden, ob das Programm sauber beendet wurde.
Hallo Illuvatar,
vielen Dank für deinen Beitrag. Ich habe die
tryLockMethode der KlasseFileChannelnicht angesprochen, da das Verhalten vonFileLocksehr systemspezifisch ist, und eine Kommunikation zwischen den beiden Applikationen nicht mehr möglich wird.Bei Gelegenheit werde ich dieses Verfahren als kleinen Zusatz aber ergänzen.
Gruß
Stefan
Super Tutorial!
Sowas hätte ich denke ich kaum zu Stande gebracht, zumindest nicht so genau
Gruß,
pcworld
SingleInstanceController.this.client.getInetAddress().getCanonicalHostName()
liefert bei mir “127.0.0.1″ statt “localhost”. Damit akzeptiert der Server den Client nicht und die Sache geht schief.
Scheint eine Eigenheit von MS-Vista zu sein? Hab’ verschiedene JDKs probiert.
Hi Stefan,
ein solches Problem ist mir nicht bekannt. Ich habe den Quellcode aber einmal ausgebessert. Anstatt von
getCanonicalHostName().equals("localhost")wird jetzt einfachisLoopbackAddress()geschrieben.Über eine Rückmeldung, ob es jetzt funktioniert oder nicht, würde ich mich freuen.
Gruß
Stefan
Hi Stefan,
ja. Das klappt jetzt auch unter Vista.
Danke und schöne Grüße,
Stefan
Ich muss in main() explizit System.exit() aufrufen sonst hängt das Programm bis ich Ctl-C drücke. Da wartet die JVM bis sich der Thread beendet.
Könnte helfen: http://java.sun.com/j2se/1.5.0/docs/api/java/lang/Thread.html#setDaemon(boolean)
Hallo Stefan,
ich kann leider gerade nicht nachvollziehen, wovon Sie reden. Das
System.exit(0);in derExampleKlasse kann auch ohne Probleme weggelassen werden.Grüße
Stefan
Nicht in der Klasse sondern im main() muss ich
exit-en sonst hängt das Programm.
public static void main(String[] args) {
new Example();
System.exit();
}
Passiert auf Ubuntu Java Version 1.6.0_14
Ich denke der exit bricht den Thread ab. Sonst wartet die JVM bis sich der Thread selbst beendet (was er nicht tut).
Kann ich leider nicht nachvollziehen, hab auch leider keine installierte Ubuntu-Version. Nur damit es zu keinen Missverständnissen kommt: Das zuerst gestartete Programm soll bis zu einem manuellen Abbruch durchlaufen. Alle weiteren Instanzen beenden sich sofort nach dem Start und dem Austausch der Nachrichten selbst.
Grüße
Stefan
Kommentar verfassen