Thread-Safety bei Netzwerkanwendungen

Ich möchte von einer CLI-Oberfläche über ein Objekt einen Server steuern. Das Objekt soll eine Liste sein und jedes Mal wenn ich etwas hinzufüge oder entferne, soll der Client bei sich diesen Befehl durchführen. Also so eine Art Synchronisation. Ich frage mich nur wie man thread-safe Daten vom Mainthread zum Serverthread oder vom Client zum Mainthread übertragen kann. Gibt’s da einen zuverlässigen Weg?

Sieht ganz so aus als wäre LinkedBlockingQueue das richtige Werkzeug oder was meint ihr?

Wenn es dann größer und professioneller zugeht, kann man auch sich überlegen Hazelcast einzusetzen. Aber das könnte in diesem Fall gut den Aufwand sprengen.

Das concurency package in java ist schon der richtige Ort, um nach den richtigen Datenstrukuren zu schauen.

Aber diese machen das ganze nicht per Definition “ThreadSafe”. Eine gute Hilfe sind hier die Apache Commons Concurency Utils.

Hallo @mdickie , wie überträgst du denn die Objekte?

Ich übertrage jedenfalls die Objekte per Serialisation. Es sind einzelne Kommandos vom Server, der der Client auf sein eigenes Objekt anwenden soll.

Sieht so aus als bräuchte ich doch keine ThreadSafety, da nur ein Thread im Programm auf die Daten schreibt und kein anderer. Beim Lesen bin ich mir da nicht so sicher. Bei der Ausführung meines Programms scheint es zumindest keine Probleme zu geben.

@mdickie
Du solltest die Liste/Queue/… nach dem producer-consumer wrappen,
denn die threadsicherheit der LinkedBlockingQueue/… wird nur sehr atomar sein.

Dann ist es wurscht, wer wie oft gleichzeitig produziert + konsumiert…

Naja, du könntest das ja mit einem Logger oder Debugger leicht feststellen.

Hier wären weitere Informationen notwendig, um sinnvoll Diskutieren zu können. Momentan beschreibst du eine ganz normale Anwendung :wink:

Da du über Netzwerk die Commandos verschickst, benötigst du keine Thread-Safety, sondern eine Quittierung und Kontolle über die ausgeführten Commandos. Da muss man sich da ein Protokoll überlegen oder ein vorhandenes adaptieren.

@Marcinek Mein gewähltes Protokoll funktioniert über Serialization. Ich habe entschieden, dass es eine Klasse Command gibt, die den Typ des Kommandos mit dem zugehörigen Operanden angibt. Um nicht alle Kommandos, die man bisher angewendet hat, an einem neuen Client zu schicken, übergebe ich dem Client eine ArrayList, die den jetzigen Zustand repräsentiert. Durch instanceof habe ich es nicht mal notwendig, die ArrayList in der Command Klasse zu wrappen. Zugebenermaßen, bisschen hacky, aber hauptsache es funktioniert.

Danke für den Tipp @CyborgBeta. habe ich mal implementiert, um komplett sicher zu gehen.

Für weitere Leser dieser Frage, die ja häufig kommt, wäre gut, wenn du beschreibst, wie du deine Liste mit dem C-P-Pattern gewrappt hast.

Die Liste, die den aktuellen Zustand repräsentiert, habe ich jetzt nicht in ein C-P Pattern gewrappt. In dem Fall frage ich mich, aber ob es vielleicht wäre es doch nicht besser wäre, die Zugriffe auf die Liste zu synchronisieren. Um jedenfalls neue Kommandos auf den Client zu übertragen, verwende ich das Pattern:

public class SyncContainer {

        private ArrayList<String> state;

	private CopyOnWriteArrayList<LinkedBlockingQueue<Command>> serverToClients;

	...

	private class ServerConsumerRunnable implements Runnable {
	
		private Socket socket;

		private LinkedBlockingQueue<Command> owninstructions;
	
		public ServerConsumerRunnable(Socket socket) {
			this.socket = socket;
			this.owninstructions = new LinkedBlockingQueue<>();
			serverToClients.add(owninstruction);
		}

		public void run() {
			try {
				ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());

		        	oos.writeObject(state); 
			
				while (true) {
					Instruction instruction = owninstructions.take();
					oos.writeObject(instruction);
				}
			} catch (Exception e) {
				e.printStackTrace();
			} finally {
				serverToClients.remove(owninstruction);
			}
	}
}

Der Producer muss einfach seine Kommandos in jeder LinkedBlockingQueue der CopyOnWriteArrayList serverToClients mit put hinzufügen. Damit versendet dann jeder ServerConsumer Threads das Kommando an seinen Client.

Wenn mir die Anmerkung gestattet ist: der Consumer müsste nicht so eng mit dem Container gekoppelt werden.
Eine lose Kopplung, bei der im Konstruktor des Consumers der Container übergeben würde, wäre wohl ausreichend.

Eine Synchronisierung der Operationen auf den Collections aus dem Concurrency-Package ist nicht erforderlich und führt im Zweifel eher zu Problemen, als zu einer Verbesserung der Threadsicherheit.
Ein Iterator und auch die Iteration über die äußere Liste in einer for-each-Schleife arbeitet immer auf einem gültigen Snapshot.

Das Verhalten ist im SyncContainer gekapselt, welcher den Producer darstellt, richtig?

Stimmt, habe mir nur gedacht, dass das vielleicht auf den Collections aus java.util sinnvoll wäre.

Nachdem du es erwähnst, fällt es mir auch auf, danke für den Tipp. :+1:

Genau jedes Mal, wenn man eine Methode aufruft, die den Container verändert, soll der SyncContainer das jeweilige Kommando in die LinkedBlockingQueues hinzufügen.