Modernes Threading: Ein Java-Primer für Parallelität

Vieles, was Sie über das Programmieren mit Java-Threads lernen müssen, hat sich im Laufe der Entwicklung der Java-Plattform nicht dramatisch geändert, aber es hat sich schrittweise geändert. In diesem Java-Thread-Primer trifft Cameron Laird einige der höchsten (und niedrigsten) Punkte von Threads als gleichzeitige Programmiertechnik. Verschaffen Sie sich einen Überblick über die ständigen Herausforderungen bei der Multithread-Programmierung und erfahren Sie, wie sich die Java-Plattform entwickelt hat, um einige der Herausforderungen zu meistern.

Parallelität gehört zu den größten Sorgen für Neulinge in der Java-Programmierung, aber es gibt keinen Grund, sich davon entmutigen zu lassen. Mit der Weiterentwicklung der Java-Plattform ist nicht nur eine hervorragende Dokumentation verfügbar (wir werden in diesem Artikel verschiedene Quellen untersuchen), sondern auch die Arbeit mit Java-Threads ist einfacher geworden. Um zu lernen, wie man Multithread-Programmierung in Java 6 und 7 ausführt, benötigen Sie wirklich nur einige Bausteine. Wir werden mit diesen beginnen:

  • Ein einfaches Thread-Programm
  • Beim Threading dreht sich alles um Geschwindigkeit, oder?
  • Herausforderungen der Java-Parallelität
  • Wann Runnable verwendet werden soll
  • Wenn gute Fäden schlecht werden
  • Was ist neu in Java 6 und 7?
  • Was kommt als nächstes für Java-Threads?

Dieser Artikel ist eine Anfängerübersicht über Java-Threading-Techniken, einschließlich Links zu einigen der am häufigsten gelesenen Einführungsartikel von JavaWorld zur Multithread-Programmierung. Starten Sie Ihre Engines und folgen Sie den obigen Links, wenn Sie noch heute etwas über Java-Threading lernen möchten.

Ein einfaches Thread-Programm

Betrachten Sie die folgende Java-Quelle.

Listing 1. FirstThreadingExample

class FirstThreadingExample { public static void main (String [] args) { // The second argument is a delay between // successive outputs. The delay is // measured in milliseconds. "10", for // instance, means, "print a line every // hundredth of a second". ExampleThread mt = new ExampleThread("A", 31); ExampleThread mt2 = new ExampleThread("B", 25); ExampleThread mt3 = new ExampleThread("C", 10); mt.start(); mt2.start(); mt3.start(); } } class ExampleThread extends Thread { private int delay; public ExampleThread(String label, int d) { // Give this particular thread a // name: "thread 'LABEL'". super("thread '" + label + "'"); delay = d; } public void run () { for (int count = 1, row = 1; row < 20; row++, count++) { try { System.out.format("Line #%d from %s\n", count, getName()); Thread.currentThread().sleep(delay); } catch (InterruptedException ie) { // This would be a surprise. } } } }

Kompilieren Sie nun diese Quelle und führen Sie sie wie jede andere Java-Befehlszeilenanwendung aus. Sie sehen eine Ausgabe, die ungefähr so ​​aussieht:

Listing 2. Ausgabe eines Thread-Programms

Line #1 from thread 'A' Line #1 from thread 'C' Line #1 from thread 'B' Line #2 from thread 'C' Line #3 from thread 'C' Line #2 from thread 'B' Line #4 from thread 'C' ... Line #17 from thread 'B' Line #14 from thread 'A' Line #18 from thread 'B' Line #15 from thread 'A' Line #19 from thread 'B' Line #16 from thread 'A' Line #17 from thread 'A' Line #18 from thread 'A' Line #19 from thread 'A'

Das war's - du bist ein Java- ThreadProgrammierer!

Na gut, vielleicht nicht so schnell. So klein das Programm in Listing 1 auch ist, es enthält einige Feinheiten, die unsere Aufmerksamkeit verdienen.

Fäden und Unbestimmtheit

Ein typischer Lernzyklus mit Programmierung besteht aus vier Phasen: (1) Neues Konzept studieren; (2) Beispielprogramm ausführen; (3) Leistung mit Erwartung vergleichen; und (4) iterieren, bis die beiden übereinstimmen. Beachten Sie jedoch, dass ich zuvor gesagt habe, dass die Ausgabe für FirstThreadingExample"so etwas wie" Listing 2 aussehen würde. Das bedeutet also, dass Ihre Ausgabe Zeile für Zeile von meiner abweichen kann. Worum geht es ?

In den einfachsten Java-Programmen gibt es eine Garantie für die Ausführungsreihenfolge: Die erste Zeile main()wird zuerst ausgeführt, dann die nächste usw. mit entsprechender Rückverfolgung in und aus anderen Methoden. Threadschwächt diese Garantie.

Threading bringt neue Möglichkeiten in die Java-Programmierung. Sie können Ergebnisse mit Threads erzielen, auf die Sie ohne sie nicht verzichten könnten. Diese Macht geht jedoch zu Lasten der Bestimmtheit . In den einfachsten Java-Programmen gibt es eine Garantie für die Ausführungsreihenfolge: Die erste Zeile main()wird zuerst ausgeführt, dann die nächste usw. mit entsprechender Rückverfolgung in und aus anderen Methoden. Threadschwächt diese Garantie. In einem Multithread-Programm wird Line #17 from thread Bmöglicherweise " " vor oder nach " Line #14 from thread A" auf Ihrem Bildschirm angezeigt , und die Reihenfolge kann bei aufeinanderfolgenden Ausführungen desselben Programms auch auf demselben Computer unterschiedlich sein.

Unbestimmtheit mag ungewohnt sein, muss aber nicht stören. Die Ausführungsreihenfolge innerhalb eines Threads bleibt vorhersehbar, und Unbestimmtheit bietet auch Vorteile. Möglicherweise haben Sie bei der Arbeit mit grafischen Benutzeroberflächen (GUIs) etwas Ähnliches erlebt. Beispiele sind Ereignis-Listener in Swing oder Event-Handler in HTML.

Während eine vollständige Erörterung der Thread-Synchronisation außerhalb des Rahmens dieser Einführung liegt, ist es einfach, die Grundlagen zu erläutern.

Betrachten Sie beispielsweise die Mechanik der HTML-Angabe ... onclick = "myFunction();" ..., um die Aktion zu bestimmen, die nach dem Klicken des Benutzers ausgeführt wird. Dieser bekannte Fall der Unbestimmtheit zeigt einige seiner Vorteile. In diesem Fall myFunction()wird dies nicht zu einem bestimmten Zeitpunkt in Bezug auf andere Elemente des Quellcodes ausgeführt, sondern in Bezug auf die Aktion des Endbenutzers . Unbestimmtheit ist also nicht nur eine Schwäche im System. Es ist auch eine Bereicherung des Ausführungsmodells, das dem Programmierer neue Möglichkeiten bietet, Sequenz und Abhängigkeit zu bestimmen.

Ausführungsverzögerungen und Thread-Unterklassen

Sie können daraus lernen, FirstThreadingExampleindem Sie selbst damit experimentieren. Versuchen Sie, ExampleThreads hinzuzufügen oder zu entfernen - das heißt, Konstruktoraufrufe wie ... new ExampleThread(label, delay);- und basteln Sie an delays. Die Grundidee ist, dass das Programm drei separate Threads startet , die dann bis zum Abschluss unabhängig voneinander ausgeführt werden. Um ihre Ausführung lehrreicher zu gestalten, verzögert sich jede geringfügig zwischen den aufeinanderfolgenden Zeilen, die sie in die Ausgabe schreibt. Dies gibt den anderen Threads die Möglichkeit, ihre Ausgabe zu schreiben .

Beachten Sie, dass für die Threadprogrammierte Programmierung im Allgemeinen keine Behandlung erforderlich ist InterruptedException. Das in gezeigte FirstThreadingExamplehat damit zu tun sleep(), anstatt direkt damit verbunden zu sein Thread. Die am meisten Threadbasierte Quelle enthält kein a sleep(); Der Zweck von sleep()hier ist es, auf einfache Weise das Verhalten von Methoden mit langer Laufzeit zu modellieren, die "in freier Wildbahn" gefunden wurden.

In Listing 1 ist noch zu beachten, dass Threades sich um eine abstrakte Klasse handelt, die für Unterklassen konzipiert ist. Die Standardmethode run()führt nichts aus und muss daher in der Unterklassendefinition überschrieben werden, um nützliche Funktionen zu erzielen.

Hier dreht sich alles um Geschwindigkeit, oder?

Inzwischen können Sie ein wenig sehen, was das Programmieren mit Threads komplex macht. Der Hauptgrund für all diese Schwierigkeiten ist jedoch nicht, an Geschwindigkeit zu gewinnen.

Multithread-Programme werden im Allgemeinen nicht schneller abgeschlossen als Single-Thread- Programme - tatsächlich können sie in pathologischen Fällen erheblich langsamer sein. Der grundlegende Mehrwert von Multithread-Programmen ist die Reaktionsfähigkeit . Wenn der JVM mehrere Verarbeitungskerne zur Verfügung stehen oder wenn das Programm viel Zeit damit verbringt, auf mehrere externe Ressourcen wie Netzwerkantworten zu warten, kann Multithreading dazu beitragen, dass das Programm schneller abgeschlossen wird.

Stellen Sie sich eine GUI-Anwendung vor: Wenn sie immer noch auf Endbenutzerpunkte und Klicks reagiert, während sie "im Hintergrund" nach einem passenden Fingerabdruck sucht oder den Kalender für das Tennisturnier des nächsten Jahres neu berechnet, wurde sie unter Berücksichtigung der Parallelität erstellt. Bei einer typischen gleichzeitigen Anwendungsarchitektur werden Benutzeraktionen in einem Thread erkannt und darauf reagiert, der von dem für die große Back-End-Last zugewiesenen Rechen-Thread getrennt ist. (Weitere Informationen zu diesen Prinzipien finden Sie unter "Swing-Threading und Event-Dispatch-Thread".)

In Ihrer eigenen Programmierung ziehen Sie es daher am wahrscheinlichsten in Betracht, Threads unter einem der folgenden Umstände zu verwenden:

  1. An existing application has correct functionality but is unresponsive at times. These "blocks" often have to do with external resources outside your control: time-consuming database queries, complicated calculations, multimedia playback, or networked responses with uncontrollable latency.
  2. A computationally-intense application could make better use of multicore hosts. This might be the case for someone rendering complex graphics or simulating an involved scientific model.
  3. Thread naturally expresses the application's required programming model. Suppose, for instance, that you were modeling the behavior of rush-hour automobile drivers or bees in a hive. To implement each driver or bee as a Thread-related object might be convenient from a programming standpoint, apart from any considerations of speed or responsiveness.

Herausforderungen der Java-Parallelität

Der erfahrene Programmierer Ned Batchelder witzelte kürzlich

Einige Leute denken, wenn sie mit einem Problem konfrontiert werden: "Ich weiß, ich werde Fäden verwenden", und dann haben sie zwei Erpoblesmen.

Das ist lustig, weil es das Problem der Parallelität so gut modelliert. Wie bereits erwähnt, liefern Multithread-Programme wahrscheinlich unterschiedliche Ergebnisse hinsichtlich der genauen Reihenfolge oder des Zeitpunkts der Thread-Ausführung. Das ist beunruhigend für Programmierer, die darin geschult sind, in reproduzierbaren Ergebnissen, strenger Bestimmtheit und unveränderlicher Reihenfolge zu denken.

It gets worse. Different threads might not only produce results in different orders, but they can contend at more essential levels for results. It's easy for a newcomer to multithreading to close() a file handle in one Thread before a different Thread has finished everything it needs to write.

Testing concurrent programs

Ten years ago on JavaWorld, Dave Dyer noted that the Java language had one feature so "pervasively used incorrectly" that he ranked it as a serious design flaw. That feature was multithreading.

Dyer's comment highlights the challenge of testing multithreaded programs. When you can no longer easily specify the output of a program in terms of a definite sequence of characters, there will be an impact on how effectively you can test your threaded code.

The correct starting point to resolving the intrinsic difficulties of concurrent programming was well stated by Heinz Kabutz in his Java Specialist newsletter: recognize that concurrency is a topic that you should understand and study it systematically. There are of course tools such as diagramming techniques and formal languages that will help. But the first step is to sharpen your intuition by practicing with simple programs like FirstThreadingExample in Listing 1. Next, learn as much as you can about threading fundamentals like these:

  • Synchronization and immutable objects
  • Thread scheduling and wait/notify
  • Race conditions and deadlock
  • Thread monitors for exclusive access, conditions, and assertions
  • JUnit best practices -- testing multithreaded code

When to use Runnable

Object orientation in Java defines singly inherited classes, which has consequences for multithreading coding. To this point, I have only described a use for Thread that was based on subclasses with an overridden run(). In an object design that already involved inheritance, this simply wouldn't work. You cannot simultaneously inherit from RenderedObject or ProductionLine or MessageQueue alongside Thread!

This constraint affects many areas of Java, not just multithreading. Fortunately, there's a classical solution for the problem, in the form of the Runnable interface. As explained by Jeff Friesen in his 2002 introduction to threading, the Runnable interface is made for situations where subclassing Thread isn't possible:

Die RunnableSchnittstelle deklariert eine einzelne Methodensignatur : void run();. Diese Signatur ist identisch mit Threadder run()Methodensignatur und dient als Ausführungseintrag für einen Thread. Da Runnablees sich um eine Schnittstelle handelt, kann jede Klasse diese Schnittstelle implementieren, indem sie eine implementsKlausel an den Klassenheader anfügt und eine geeignete run()Methode bereitstellt . Zur Ausführungszeit kann der Programmcode aus dieser Klasse ein Objekt oder eine ausführbare Datei erstellen und die Referenz der ausführbaren Datei an einen geeigneten ThreadKonstruktor übergeben.

Für Klassen, die nicht erweitert werden können Thread, müssen Sie eine ausführbare Datei erstellen, um Multithreading nutzen zu können. Wenn Sie semantisch programmieren und Ihre Klasse in einer is-a-Beziehung zu steht Thread, sollten Sie semantisch direkt eine Unterklasse von Thread. Die meisten Anwendungen von Multithreading auf Anwendungsebene hängen jedoch von der Komposition ab und definieren daher eine RunnableKompatibilität mit dem Klassendiagramm der Anwendung. Glücklicherweise sind nur ein oder zwei zusätzliche Zeilen erforderlich, um über die RunnableSchnittstelle zu codieren , wie in Listing 3 unten gezeigt.