9 zu vermeidende Fehler bei der Programmierung in Java
Welche Fehler sollten bei der Programmierung in Java vermieden werden? Im folgenden Beitrag beantworten wir diese Frage.
Lesen Sie den ersten Teil unserer Blogserie, die sich mit der Gleichzeitigkeit in Java beschäftigt. Im folgenden Artikel werden wir uns die Unterschiede zwischen Threads und Prozessen, Thread-Pools, Executors und vieles mehr genauer ansehen!
Im Allgemeinen ist der herkömmliche Programmieransatz sequentiell. Alles in einem Programm geschieht einen Schritt nach dem anderen.
Aber in Wirklichkeit ist es die Parallele, wie die ganze Welt läuft - es ist die Fähigkeit, mehr als eine Aufgabe gleichzeitig auszuführen.
Zur Erörterung fortgeschrittener Themen wie Gleichzeitigkeit in Java oder Multithreading, müssen wir uns auf einige gemeinsame Definitionen einigen, um sicher zu sein, dass wir auf derselben Seite stehen.
Lassen Sie uns mit den Grundlagen beginnen. In der nichtsequenziellen Welt gibt es zwei Arten von Gleichzeitigkeitsrepräsentanten: Prozesse und
Threads. Ein Prozess ist eine Instanz des laufenden Programms. Normalerweise ist er von anderen Prozessen isoliert.
Das Betriebssystem ist für die Zuweisung von Ressourcen an jeden Prozess verantwortlich. Außerdem fungiert es als Dirigent, der
plant und kontrolliert sie.
Ein Thread ist eine Art Prozess, aber auf einer niedrigeren Ebene, daher wird er auch als leichter Thread bezeichnet. Mehrere Threads können in einem
Prozess. Hier fungiert das Programm als Scheduler und Controller für Threads. Auf diese Weise scheinen einzelne Programme
mehrere Aufgaben gleichzeitig zu erledigen.
Der grundlegende Unterschied zwischen Threads und Prozessen ist die Isolationsebene. Der Prozess hat seinen eigenen Satz von
Ressourcen, während der Thread Daten mit anderen Threads teilt. Dies mag wie ein fehleranfälliger Ansatz erscheinen und ist es auch. Für
Aber darauf wollen wir uns jetzt nicht konzentrieren, denn das würde den Rahmen dieses Artikels sprengen.
Prozesse, Threads - ok... Aber was genau ist Gleichzeitigkeit? Gleichzeitigkeit bedeutet, dass Sie mehrere Aufgaben gleichzeitig ausführen können.
Zeit. Das bedeutet nicht, dass diese Aufgaben gleichzeitig ablaufen müssen - das ist die Aufgabe der Parallelität. Concurrenc in Javay auch nicht
erfordern nicht, dass Sie mehrere CPUs oder sogar mehrere Kerne haben. In einer Single-Core-Umgebung kann dies erreicht werden durch die Nutzung von
Kontextwechsel.
Ein mit der Gleichzeitigkeit verbundener Begriff ist Multithreading. Dies ist eine Eigenschaft von Programmen, die es ihnen ermöglicht, mehrere Aufgaben gleichzeitig auszuführen. Nicht jedes Programm verwendet diesen Ansatz, aber diejenigen, die dies tun, können als multithreaded bezeichnet werden.
Wir sind fast fertig, nur noch eine Definition. Asynchronität bedeutet, dass ein Programm nicht-blockierende Operationen durchführt.
Er leitet eine Aufgabe ein und macht dann mit anderen Dingen weiter, während er auf die Antwort wartet. Wenn sie die Antwort erhält, kann sie darauf reagieren.
Standardmäßig wird jede Java-Anwendung läuft in einem einzigen Prozess. In diesem Prozess gibt es einen Thread, der mit dem main()
Methode der
einer Anwendung. Wie bereits erwähnt, ist es jedoch möglich, die Mechanismen mehrerer Threads innerhalb einer Anwendung zu nutzen.
Programm.
Thema
ist eine Java Klasse, in der die Magie stattfindet. Dies ist die Objektdarstellung des zuvor erwähnten Threads. An
einen eigenen Thread zu erstellen, können Sie die Thema
Klasse. Dies ist jedoch kein empfohlener Ansatz. Fäden
sollte als Mechanismus zur Ausführung der Aufgabe verwendet werden. Tasks sind Teile von Code die wir im gleichzeitigen Modus ausführen wollen. Wir können sie mit der Option Lauffähig
Schnittstelle.
Aber genug der Theorie, lassen Sie uns unseren Code dort einsetzen, wo wir ihn brauchen.
Angenommen, wir haben eine Reihe von Zahlenfeldern. Für jede Reihe wollen wir die Summe der Zahlen in einer Reihe wissen. Nehmen wir an
vorgeben, dass es eine Menge solcher Arrays gibt und jedes von ihnen relativ groß ist. Unter solchen Bedingungen wollen wir die Gleichzeitigkeit nutzen und jedes Array als separate Aufgabe summieren.
int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] a2 = {10, 10, 10, 10, 10, 10, 10, 10, 10};
int[] a3 = {3, 4, 3, 4, 3, 4, 2, 1, 3, 7};
Runnable task1 = () -> {
int sum = Arrays.stream(a1).sum();
System.out.println("1. Die Summe ist: " + Summe);
};
Runnable task2 = () -> {
int sum = Arrays.stream(a2).sum();
System.out.println("2. Die Summe ist: " + Summe);
};
Runnable task3 = () -> {
int sum = Arrays.stream(a3).sum();
System.out.println("3. Die Summe ist: " + Summe);
};
new Thread(task1).start();
new Thread(task2).start();
new Thread(task3).start();
Wie Sie aus dem obigen Code ersehen können Lauffähig
ist eine funktionale Schnittstelle. Sie enthält eine einzige abstrakte Methode run()
ohne Argumente. Die Lauffähig
Schnittstelle sollte von jeder Klasse implementiert werden, deren Instanzen dazu bestimmt sind
von einem Thread ausgeführt.
Sobald Sie eine Aufgabe definiert haben, können Sie einen Thread erstellen, um sie auszuführen. Dies kann erreicht werden durch neuer Thread()
Konstrukteur, der
nimmt Lauffähig
als sein Argument.
Der letzte Schritt ist start()
einen neu erstellten Thread. In der API gibt es auch run()
Methoden in Lauffähig
und inThema
. Dies ist jedoch keine Möglichkeit, die Gleichzeitigkeit in Java zu nutzen. Ein direkter Aufruf jeder dieser Methoden führt zu
die Ausführung der Aufgabe im selben Thread, in dem die main()
Methode läuft.
Wenn es viele Aufgaben gibt, ist es keine gute Idee, für jede Aufgabe einen eigenen Thread zu erstellen. Das Erstellen eines Thema
ist eine
und es ist weitaus besser, bestehende Threads wiederzuverwenden, als neue zu erstellen.
Wenn ein Programm viele kurzlebige Threads erzeugt, ist es besser, einen Thread-Pool zu verwenden. Der Thread-Pool enthält eine Anzahl von
betriebsbereite, aber derzeit nicht aktive Threads. Das Geben eines Lauffähig
zum Pool führt dazu, dass einer der Threads die Funktionrun()
Methode der gegebenen Lauffähig
. Nach Abschluss einer Aufgabe ist der Thread noch vorhanden und befindet sich in einem Leerlaufzustand.
Ok, Sie haben es verstanden - Sie bevorzugen Thread-Pools anstelle der manuellen Erstellung. Aber wie können Sie Thread-Pools nutzen? Die Testamentsvollstrecker
Klasse verfügt über eine Reihe von statischen Fabrikmethoden zur Erstellung von Thread-Pools. Zum Beispiel newCachedThredPool()
erstellt
einen Pool, in dem bei Bedarf neue Threads erstellt werden und inaktive Threads 60 Sekunden lang gehalten werden. Im Gegensatz dazu,newFixedThreadPool()
enthält einen festen Satz von Threads, in dem inaktive Threads auf unbestimmte Zeit gehalten werden.
Schauen wir uns an, wie das in unserem Beispiel funktionieren könnte. Jetzt müssen wir die Threads nicht mehr manuell erstellen. Stattdessen müssen wirExecutorService
der einen Pool von Threads bereitstellt. Dann können wir ihm Aufgaben zuweisen. Der letzte Schritt besteht darin, den Thread zu schließen
Pool, um Speicherlecks zu vermeiden. Der Rest des bisherigen Codes bleibt unverändert.
ExecutorService executor = Executors.newCachedThreadPool();
executor.submit(task1);
executor.submit(task2);
executor.submit(task3);
executor.shutdown();
Lauffähig
scheint eine elegante Methode zur Erstellung gleichzeitiger Aufgaben zu sein, aber sie hat einen großen Mangel. Sie kann keine
Wert. Darüber hinaus können wir nicht feststellen, ob eine Aufgabe abgeschlossen ist oder nicht. Wir wissen auch nicht, ob sie abgeschlossen wurde
normal oder außergewöhnlich. Die Lösung für diese Übel ist Abrufbar
.
Abrufbar
ist vergleichbar mit Lauffähig
in gewisser Weise verpackt es auch asynchrone Aufgaben. Der Hauptunterschied besteht darin, dass sie in der Lage ist
einen Wert zurückgeben. Der Rückgabewert kann von jedem (nicht-primitiven) Typ sein, da die Abrufbar
Schnittstelle ist ein parametrisierter Typ.Abrufbar
ist eine funktionale Schnittstelle, die über Aufruf()
Methode, die ein Ausnahme
.
Jetzt wollen wir sehen, wie wir die Abrufbar
in unserem Array-Problem.
int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] a2 = {10, 10, 10, 10, 10, 10, 10, 10, 10};
int[] a3 = {3, 4, 3, 4, 3, 4, 2, 1, 3, 7};
Callable task1 = () -> Arrays.stream(a1).sum();
Callable task2 = () -> Arrays.stream(a2).sum();
Callable task3 = () -> Arrays.stream(a3).sum();
ExecutorService executor = Executors.newCachedThreadPool();
Future future1 = executor.submit(task1);
Future future2 = executor.submit(task2);
Future future3 = executor.submit(task3);
System.out.println("1. Die Summe ist: " + future1.get());
System.out.println("2. Die Summe ist: " + future2.get());
System.out.println("3. die Summe ist: " + future3.get());
executor.shutdown();
Okay, wir können sehen, wie Abrufbar
wird erstellt und dann an ExecutorService
. Aber was zum Teufel ist Zukunft
?Zukunft
fungiert als Brücke zwischen den Threads. Die Summe jedes Arrays wird in einem separaten Thread erzeugt und wir brauchen einen Weg, um
diese Ergebnisse zurück an main()
.
So rufen Sie das Ergebnis von Zukunft
müssen wir anrufen get()
Methode. Hier kann eines von zwei Dingen passieren. Erstens, die
Ergebnis der Berechnung, die von Abrufbar
verfügbar ist. Dann bekommen wir es sofort. Zweitens, das Ergebnis ist nicht
noch nicht fertig. In diesem Fall get()
Methode wird blockiert, bis das Ergebnis vorliegt.
Das Problem mit Zukunft
ist, dass es nach dem "Push-Paradigma" funktioniert. Bei der Verwendung Zukunft
muss man wie ein Chef sein, der
fragt ständig: "Ist Ihre Aufgabe erledigt? Ist sie fertig?", bis sie ein Ergebnis liefert. Unter ständigem Druck zu handeln ist
teuer. Ein weitaus besserer Ansatz wäre es, zu bestellen Zukunft
was zu tun ist, wenn es mit seiner Aufgabe fertig ist. Leider,Zukunft
kann das nicht tun, aber ComputableFuture
können.
ComputableFuture
arbeitet im "Pull-Paradigma". Wir können ihm sagen, was es mit dem Ergebnis tun soll, wenn es seine Aufgaben erfüllt hat. Es
ist ein Beispiel für einen asynchronen Ansatz.
ComputableFuture
funktioniert perfekt mit Lauffähig
aber nicht mit Abrufbar
. Stattdessen ist es möglich, eine Aufgabe zu liefernComputableFuture
in Form von Anbieter
.
Schauen wir uns an, wie sich die obigen Ausführungen auf unser Problem beziehen.
int[] a1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] a2 = {10, 10, 10, 10, 10, 10, 10, 10, 10};
int[] a3 = {3, 4, 3, 4, 3, 4, 2, 1, 3, 7};
CompletableFuture.supplyAsync(() -> Arrays.stream(a1).sum())
.thenAccept(System.out::println);
CompletableFuture.supplyAsync(() -> Arrays.stream(a2).sum())
.thenAccept(System.out::println);
CompletableFuture.supplyAsync(() -> Arrays.stream(a3).sum())
.thenAccept(System.out::println);
Das erste, was einem auffällt, ist, wie viel kürzer diese Lösung ist. Außerdem sieht sie auch noch ordentlich und aufgeräumt aus.
Aufgabe an ErfüllbarZukunft
kann bereitgestellt werden von supplyAsync()
Methode, die die Anbieter
oder durch runAsync()
dass
nimmt Lauffähig
. Ein Callback - ein Stück Code, das bei Abschluss der Aufgabe ausgeführt werden soll - wird definiert durch thenAccept()
Methode.
Java bietet viele verschiedene Ansätze zur Gleichzeitigkeit. In diesem Artikel haben wir das Thema nur am Rande behandelt.
Dennoch haben wir die Grundlagen der Thema
, Lauffähig
, Abrufbar
und CallableFuture
was ein gutes Argument ist
zur weiteren Untersuchung des Themas.