Ganz offenbar können sich Threads gegenseitig Informationen überreichen und Methoden im anderen Thread aufrufen.
Ich möchte mich hier vergewissern, ob das - funktionierende - Verfahren, wie ich es anwende, auch gut / so gedacht ist, oder ob ich etwas anders machen sollte.
Zur Abstraktion habe ich ein kleines KKSB (das so für sich natürlich unnütz ist, aber es skizziert grob das Vorggehen) zusammengestellt:
public class A {
public static void main(String[] args) {
new A();
}
public A() {
System.out.println("A - Start");
B b = startB();
b.say("Hallo");
sleep();
b.say("Jaha!");
sleep();
b.say("Tschüß");
b.quit();
System.out.println("A - Ende");
}
private B startB() {
B b = new B();
Thread t = new Thread(b);
t.start();
return b;
}
private void sleep() {
try {
Thread.sleep(300);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class B implements Runnable {
private boolean running;
@Override
public void run() {
System.out.println("B - Start");
running = true;
while (running) {
System.out.println("B tut was");
sleep();
}
System.out.println("B - Ende");
}
private void sleep() {
try {
Thread.sleep(100);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
public void quit() {
running = false;
}
public void say(String message) {
System.out.println("B sagt: " + message);
}
}
mit der Ausgabe:
A - Start
B sagt: Hallo
B - Start
B tut was
B tut was
B tut was
B sagt: Jaha!
B tut was
B tut was
B tut was
B sagt: Tschüß
B tut was
A - Ende
B - Ende
Ganz offenbar funktioniert das Übergeben von Nachrichten von Thread A an Thread B (ich nenne sie mal nach ihren Klassen). Und da say von B ausgeführt wird, sollte die Ausgabe auch wirklich im Thread B erfolgen und nicht im Thread A.
Ist das die feine englische Art, Daten auf diese Weise zu übergeben, oder allgemeiner, Methoden von einem anderen Thread aus zu starten?
Oder sollte ich so etwas wie invokeLater() in B einbauen, wie es Swing etwa vormacht?
Nein, da sind einige nicht sonderlich schöne Fehler drin.
Als erstes fiel mir auf, dass es möglich ist, dass Thread B niemals terminiert. Das liegt daran, dass die private Variable running nicht unbedingt aktualisiert wird, bevor sie von Thread B ausgelesen wird. Dazu solltest du sie mit volatile absichern oder die quit-Methode mit synchronized schützen.
Nur, weil die Methode B#say() sagt, dass sie in Thread B ausgeführt wird, wird sie das noch lange nicht. Der Debugger gibt dir darüber Aufschluss.
running wird auf false gesetzt. Aber Thread B sieht das nicht unbedingt, weil die JVM nicht garantiert, dass die Daten beim Auslesen durch einen anderen Thread neu geladen werden. Dieses “neu laden” erzwingt man mit volatile.
[QUOTE=Crian]Oh. Das ist ja äußerst unpraktisch.
[/QUOTE]
das ist weder praktisch noch unpraktisch, sondern ganz normal,
eher sogar positiv,
stelle dir dich selber als laufender Thread vor, deinen Schreibtisch als Thread-Objekt,
der ist zwar ziemlich auf dich ausgerichtet, dennoch einfach nur ein Objekt, ein Schreibtisch,
jeder andere kann da auch einen Brief unterschreiben, wäre fatal wenn das dann ohne dein Wissen auf dich zurückfiele
du kannst den String in einer Variablen/ Liste ablegen und die run-Methode schaut beizeiten vorbei und gibt den String aus,
genau wie ein Brief auf den Schreibtisch abgelegt werden kann so dass du ihn beizeiten unterschreibst
Ah, danke. Also in meinen Worten: Weil quit() - von A aufgerufen - mitnichten, wie ich fälschlich dachte, im Thread B, sondern im Thread A läuft, muss Thread B darauf hingewiesen werden, dass die Variable von anderer Seite geändert werden könnte.
@SlaterB : Sehr nettes Beispiel. grinst
Ja, sowas meinte ich mit invokeLater, da ist es wohl auch so, dass Dinge in einem Vorrat abgelegt und dann in der Run-Methode zu passender Zeit hervorgenommen und “ausgeführt” werden.
Also wenn ich ein wirkliches Ausführen von Say im Thread B möchte, müsste ich genau so vorgehen.
Edit: Also mus sich aufpassen, wenn in B Methoden sind, die B aus run() heraus aufruft, dann sind sie in einem anderen Thread, als solche, die von außen angestoßen werden. Natürlich unterscheiden sie sich ertstmal durch public und private, aber sobald eine public-Methode wiederum eine private-Methode verwendet, die auch von run aus verwendet wird, würde die Methode mal im einen, mal im anderen Thread ausgeführt. Das meinte ich oben mit unpraktisch.
Aber gut. Wie eigentlich immer muss man nur genau wissen, was man tut. Umso verwirrender, dass all meine Threahaltigen Programme “funktionieren”. Aber ich werde dort nun nach solchen Dingen ausschau halten, um sie zu synchronisieren oder mit volatile zu versehen.
So kannst du dir das in etwa vorstellen.
Das liegt daran, wie die JVM den Speicher verwaltet. Und ein volatile bewirkt soviel wie: “sehe jedes Mal, wenn du auf die Variable zugreifst nach, ob sich der Wert geändert hat”. Ansonsten wird einfach der “zwischengespeicherte” Wert verwendet.
Kurze Anmerkung noch zum synchronized: wenn quit durch synchronized geschützt werden soll, muss auch noch die Abfrage in B.java, Zeile 11 geschützt werden, was bei einer Abfrage ohne Getter nicht funktioniert (weil du ansonsten einen DeadLock hervorrufst).
Andere Frage: Dinge, die ich im Konstruktor von B erzeuge, erzeuge ich ja noch im Thread A, da erst mit run() der zweite Thread startet. der Zweite Thread übernimmt diese dann, wenn sie sich von außen nicht ändern, sondern nur innerhalb vom Thread B, müssen sie dann nicht volatile sein, oder? Oder wäre es besserer Stil, diese dann nicht im Konstruktor, sondern in run() anzulegen?
Es ist richtig, die Objekte im Konstruktor zu erzeugen. Es gibt eine Zusicherung, die besagt, dass alles, was vor dem Aufruf von [japi]Thread#start()[/japi] passiert, dem Thread bekannt ist.
Wenn dich das Thema Threading interessiert, kann ich dir Java Concurrency in Practice von Brian Goetz uneingeschränkt empfehlen - es ist jeden Cent wert.
Ich habe nun eine Möglichkeit eingebaut “wirklich” im zweiten Thread etwas zu sagen. Wäre das nun sauberer? Du sprachst vorhin von mehreren unschönen Dingen.
public class A {
public static void main(String[] args) {
new A();
}
public A() {
System.out.println("A - Start");
B b = startB();
b.say("Hallo");
sleep();
b.sayRealInB("Jaha!");
sleep();
b.say("Tschüß");
b.quit();
System.out.println("A - Ende");
}
private B startB() {
B b = new B();
Thread t = new Thread(b);
t.start();
return b;
}
private void sleep() {
try {
Thread.sleep(300);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
import java.util.ArrayList;
import java.util.List;
public class B implements Runnable {
private volatile boolean running;
private volatile List<String> messageQueue;
@Override
public void run() {
System.out.println("B - Start");
messageQueue = new ArrayList<>();
running = true;
while (running) {
System.out.println("B tut was");
if (!messageQueue.isEmpty()) {
String message = messageQueue.remove(messageQueue.size()-1);
say(message);
}
sleep();
}
System.out.println("B - Ende");
}
private void sleep() {
try {
Thread.sleep(100);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
public void quit() {
running = false;
/*
* Weil quit() - von A aufgerufen - mitnichten, wie ich fälschlich
* dachte, im Thread B, sondern im Thread A läuft, muss Thread B darauf
* hingewiesen werden, dass die Variable von anderer Seite geändert
* werden könnte.
*/
}
public void say(String message) {
Thread current = Thread.currentThread();
System.out.println("B sagt: " + message + " im Thread " + current);
}
public void sayRealInB(String message) {
messageQueue.add(message);
}
}
Ausgabe:
A - Start
B sagt: Hallo im Thread Thread[main,5,main]
B - Start
B tut was
B tut was
B tut was
B tut was
B sagt: Jaha! im Thread Thread[Thread-0,5,main]
B tut was
B tut was
B tut was
B sagt: Tschüß im Thread Thread[main,5,main]
A - Ende
B - Ende
Und wo ist der Danke-Button hin, wenn man ihn braucht? Das Buch notiere ich mir mal, mal schauen, wie tief ich in die Materie eintauchen möchte. Vielen Dank jedenfalls für eure Hilfe so weit. Und schön, dass ich die Sachen im Konstruktor (wo sie gefühlsmäßig hingehören) belassen kann. Genauso hatte ich es gehofft, dass der neue Thread sich am Anfang alles bekannt macht.
Ein bisschen besser ist das schon. Das volatile bei der List reicht aber nicht aus, weil nur die Referenz auf die Liste “aktualisiert” wird.
Besser ist es die threadsicheren Varianten der Collections zu verwenden, die sich im Package [japi]java.util.concurrent[/japi] befinden.
Insbesondere eignet sich für deine Anwendung die [japi]BlockingQueue[/japi].
Das volatile bei der List (bzw. dann bei der BlockingQueue) solltest du weglassen, das bringt keinen Vorteil und schlägt sich nur auf die Performance nieder.
um es genau zu nehmen:
wenn die Liste nur einmalig am Anfang erzeugt wird, sollte volatile hier nicht nötig sein,
es bleibt ja immer dieselbe Liste,
schaden mag es aber auch nicht zu sehr
messageQueue.remove(messageQueue.size()-1);
ist unschöner Code, in einer tatsächlichen Queue statt List wird es da doch wohl besseres geben,
vielleicht auch immer Element 0 entfernen
und nun wird es wichtig:
der Zugriff auf die Liste ist zu synchronisieren, das ist ja nun das A und O bei gemeinsam genutzten Daten,
Ich hab das Kapitel gerade mal durchgescrollt - das sieht relativ dünn aus. Im Kapitel 12.5.2. steht beispielsweise nichts von den threadsicheren Listen (z. B. CopyOnWriteArrayList) drin.
Wie oben auch schon geschrieben, ist es nicht sinnvoll zu versuchen den Zugriff auf eine “normale” List zu synchronisieren. Es ist viel besser, eine Klasse aus dem concurrent Package zu verwenden. Dort gibt es auch Implementierungen, die nicht blockieren und somit durch nebenläufigen Zugriff keine Laufzeitverschlechterung verursachen.
Danke euch beiden. Ich habe B nun soweit verändert:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class B implements Runnable {
private volatile boolean running;
private BlockingQueue<String> messageQueue;
public B() {
messageQueue = new LinkedBlockingQueue<>();
}
@Override
public void run() {
System.out.println("B - Start");
running = true;
while (running) {
System.out.println("B tut was");
if (!messageQueue.isEmpty()) {
String message = messageQueue.remove();
say(message);
}
sleep();
}
System.out.println("B - Ende");
}
private void sleep() {
try {
Thread.sleep(100);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
public void quit() {
running = false;
}
public void say(String message) {
Thread current = Thread.currentThread();
System.out.println("B sagt: " + message + " im Thread " + current);
}
public void sayRealInB(String message) {
messageQueue.add(message);
}
}```
Ja ich hatte #10 gelesen, das Buch ist schon gebookmarked (gibt es da ein elegantes deutsches Wort für?). Aber was ihr sagt, ist natürlich richtig, die Liste blieb ja gleich, nur ihr Inhalt wurde verändert.
[QUOTE=cmrudolph]
Wie oben auch schon geschrieben, ist es nicht sinnvoll zu versuchen den Zugriff auf eine “normale” List zu synchronisieren. Es ist viel besser, eine Klasse aus dem concurrent Package zu verwenden. Dort gibt es auch Implementierungen, die nicht blockieren und somit durch nebenläufigen Zugriff keine Laufzeitverschlechterung verursachen.[/QUOTE]
genau wie nicht eine Liste jede Klasse mit get/set und Index ersetzt kann nicht das concurrent Package synchronized überflüssig machen oder für Listen verbieten,
vielleicht möchte man neben Einfügen in die Liste noch 3 weitere Variablen ändern, dann sieht man ‘nur mit concurrent Package’ alt aus,
oder wenn man einen Eintrag lesen will, bearbeiten, und 5 Zeilen später wieder etwas schreiben,
den ganze Block mag man in einem gesicherten Bereich sehen
wenn man überhaupt keine Liste sondern eine eigene Klasse hat, kommt man ohne synchronized gar nicht mehr weiter,
Standardklassen sind schön und gut, aber nie sollte man damit die allgemeinen Sprachmechnismen außer Acht lassen,
eine Liste mit Synchronisierung zu verwenden ist immer normal und gut,
Hinweis auf bessere Lösungen schaden freilich nicht
@SlaterB : dann habe ich das vielleicht ein wenig zu pauschal ausgedrückt. Was ich meinte ist, dass man für die meisten Anwendungen wohl mit den Klassen aus dem besagten Package gut bedient ist. In diesem Falle ist eine BlockingQueue perfekt geeignet. @Crian :
Ich habe den Code noch ein wenig umgestrickt. So würde ich das machen:
A.java
public class A {
private Thread threadB;
public A() {
System.out.println("A - Start");
B b = startB();
b.say("Hallo");
sleep();
b.sayRealInB("Jaha!");
sleep();
b.say("Tschüß");
threadB.interrupt();
System.out.println("A - Ende");
}
public static void main(String[] args) {
new A();
}
private B startB() {
B b = new B();
threadB = new Thread(b);
threadB.start();
return b;
}
private void sleep() {
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}```
B.java:
```package threads.question;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class B implements Runnable {
private BlockingQueue<String> messageQueue;
public B() {
messageQueue = new LinkedBlockingQueue<String>();
}
@Override
public void run() {
System.out.println("B - Start");
while (!Thread.currentThread().isInterrupted()) {
System.out.println("B tut was");
String message;
try {
message = messageQueue.take();
} catch (InterruptedException e) {
break; // let thread exit
}
say(message);
}
System.out.println("B - Ende");
}
public void say(String message) {
Thread current = Thread.currentThread();
System.out.println("B sagt: " + message + " im Thread " + current);
}
public void sayRealInB(String message) {
try {
messageQueue.put(message);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}```
Klasse A ist nicht wirklich schön, aber mir ging es erstmal um Klasse B.
Die Unterschiede sind take statt remove und die beiden try-catch-Blöcke um einfüllen / abrufen der Daten in / von der Queue, oder hab ich noch was übersehen? Ah nein, du hast das ganze running entfernt. Warum?
Klasse A ist nur ein Hilfskonstrukt. Normalerweise wird da nicht gewartet und so, das hab ich nur gemacht, um einen zeitlichen Ablauf zu simulieren.
Es gibt kein boolean running mehr, sondern der Thread B läuft endlos, bis er einen Interrupt bekommt.
Außerdem ist die quit()-Methode weg.
Falls es jemand anders machen würde, würde ich mich über Alternativvorschläge freuen.
Ist es vielleicht doch sinnvoll, eine quit()-Methode anzubieten, um einen spezifizierten Terminierungspunkt zu erhalten? Dort könnte der Thread dann ggf. interrupted werden.