Kürzlich hatte ich die Gelegenheit einen sehr interessanten Vortrag von Adam Bien anzuhören, der unter anderem Future in einem Teil seiner Beispielanwendung benutzt hat. Future bietet einen sehr eleganten Ansatz um Aufgaben in Java parallel auszuführen. Warum es also nicht mal selber in einem kleinen Beispiel ausprobieren und bei der Gelegenheit das Ganze in einen kurzen Blogbeitrag verpacken. Also los geht’s.
Der beste Anfang ist hier erstmal unsere Beispielklasse:
1package de.codecentric.blog.sample;
2
3import java.util.ArrayList;
4import java.util.Calendar;
5import java.util.List;
6import java.util.concurrent.Callable;
7import java.util.concurrent.ExecutionException;
8import java.util.concurrent.ExecutorService;
9import java.util.concurrent.Executors;
10import java.util.concurrent.FutureTask;
11
12public class FutureTaskDemo {
13
14 /**
15 * Maximum amount of numbers to check
16 */
17 public static final int MAX_NUMBER = 2000000000;
18
19 /**
20 * Returns the amount of numbers that can be divided by the divisor without remainder.
21 * @param first First number to check
22 * @param last Last number to check
23 * @param divisor Divisor
24 * @return Amount of numbers that can be divided by the divisor without remainder
25 */
26 public static int amountOfDivisibleBy(int first, int last, int divisor) {
27
28 int amount = 0;
29 for (int i = first; i <= last; i++) {
30 if (i % divisor == 0) {
31 amount++;
32 }
33 }
34 return amount;
35 }
36
37 /**
38 * Returns the amount of numbers that can be divided by the divisor without remainder (using parallel execution).
39 * @param first First number to check
40 * @param last Last number to check
41 * @param divisor Divisor
42 * @return Amount of numbers that can be divided by the divisor without remainder
43 * @throws InterruptedException
44 * @throws ExecutionException
45 */
46 public static int amountOfDivisibleByFuture(final int first, final int last, final int divisor)
47 throws InterruptedException, ExecutionException {
48
49 int amount = 0;
50
51 // Prepare to execute and store the Futures
52 int threadNum = 2;
53 ExecutorService executor = Executors.newFixedThreadPool(threadNum);
54 List<FutureTask<Integer>> taskList = new ArrayList<FutureTask<Integer>>();
55
56 // Start thread for the first half of the numbers
57 FutureTask<Integer> futureTask_1 = new FutureTask<Integer>(new Callable<Integer>() {
58 @Override
59 public Integer call() {
60 return FutureTaskDemo.amountOfDivisibleBy(first, last / 2, divisor);
61 }
62 });
63 taskList.add(futureTask_1);
64 executor.execute(futureTask_1);
65
66 // Start thread for the second half of the numbers
67 FutureTask<Integer> futureTask_2 = new FutureTask<Integer>(new Callable<Integer>() {
68 @Override
69 public Integer call() {
70 return FutureTaskDemo.amountOfDivisibleBy(last / 2 + 1, last, divisor);
71 }
72 });
73 taskList.add(futureTask_2);
74 executor.execute(futureTask_2);
75
76 // Wait until all results are available and combine them at the same time
77 for (int j = 0; j < threadNum; j++) {
78 FutureTask<Integer> futureTask = taskList.get(j);
79 amount += futureTask.get();
80 }
81 executor.shutdown();
82
83 return amount;
84 }
85
86 /**
87 * Executing the example.
88 * @param args Command line arguments
89 * @throws ExecutionException
90 * @throws InterruptedException
91 */
92 public static void main(String[] args) throws InterruptedException, ExecutionException {
93
94 // Sequential execution
95 long timeStart = Calendar.getInstance().getTimeInMillis();
96 int result = FutureTaskDemo.amountOfDivisibleBy(0, MAX_NUMBER, 3);
97 long timeEnd = Calendar.getInstance().getTimeInMillis();
98 long timeNeeded = timeEnd - timeStart;
99 System.out.println("Result : " + result + " calculated in " + timeNeeded + " ms");
100
101 // Parallel execution
102 long timeStartFuture = Calendar.getInstance().getTimeInMillis();
103 int resultFuture = FutureTaskDemo.amountOfDivisibleByFuture(0, MAX_NUMBER, 3);
104 long timeEndFuture = Calendar.getInstance().getTimeInMillis();
105 long timeNeededFuture = timeEndFuture - timeStartFuture;
106 System.out.println("Result (Future): " + resultFuture + " calculated in " + timeNeededFuture + " ms");
107 }
108}
Der hier implementierte Algorithmus (nicht sicher, ob ich es wirklich einen Algorithmus nennen sollte ;)) prüft für eine gegebene Menge von Zahlen, wie viele von diesen durch eine bestimmte Zahl teilbar sind. Dies soll wirklich nur als Beispiel für die Implementierungstechnik dienen, natürlich könnten wir auch berechnen warum 42 die Antwort auf die ultimative Frage des Lebens, der Universums uns allem Anderen ist . Für den Moment möchte ich persönlich aber doch eher bei den trivialen Problemen bleiben.
Die main-Methode wird nur gebraucht, um die beiden Varianten der Berechnung aufzurufen und damit die Zeit für die Ausführung zu berechnen. Die erste Methode amountOfDivisibleBy ist dabei wirklich trivial und braucht denke ich keine weitere Erläuterung. Bei der zweiten Methode amountOfDivisibleByFuture wird es interessant.
Zunächst brauchen wir hier einen Executor, der später benutzt wird um die Future-Tasks zu starten. Die Tasks werden zur späteren Verarbeitung in einer Liste abgelegt:
1int threadNum = 2; 2 ExecutorService executor = Executors.newFixedThreadPool(threadNum); 3 List<FutureTask<Integer>> taskList = new ArrayList<FutureTask<Integer>>();
Um dieses Beispiel möglichst einfach zu halten werden die beiden Future-Instanzen hart-codiert erzeugt, zur Liste hinzugefügt und dann mit Hilf des Executors ausgeführt. Und jetzt kommt der Kniff an der ganzen Sache. Wir haben jetzt zwei Objekte, welche die Ausführung unseres Algorithmus übernehmen.
1// Start thread for the first half of the numbers
2 FutureTask<Integer> futureTask_1 = new FutureTask<Integer>(new Callable<Integer>() {
3 @Override
4 public Integer call() {
5 return FutureTaskDemo.amountOfDivisibleBy(first, last / 2, divisor);
6 }
7 });
8 taskList.add(futureTask_1);
9 executor.execute(futureTask_1);
Für diese Objekte gibt es nun verschiedene Möglichkeiten zu prüfen, ob die Verarbeitung bereits fertig ist. In diesem Beispiel macht es Sinn in einer Schleife den „blockierenden Aufruf“ get auf unseren Future-Objekten auszuführen. ein solcher Aufruf kommt erst zurück, wenn die Bearbeitung abgeschlossen ist, d.h. der erste Aufruf wird vermutlich länger dauern und der zweite wird fast direkt ein Ergebnis zurückliefern, vorausgesetzt die Hardware ermöglicht eine schnelle parallel Verarbeitung. Die Ergebnisse der beiden Teilaufgaben werden dann nur noch addiert und zurück geliefert am Ende der Methode
1// Wait until all results are available and combine them at the same time 2 for (int j = 0; j < threadNum; j++) { 3 FutureTask<Integer> futureTask = taskList.get(j); 4 amount += futureTask.get(); 5 } 6 executor.shutdown();
Und tatsächlich kann man sehen, dass die Methode welche Future nutzt fast doppelt so schnell ist wie die rein sequentielle Verarbeitung.
Result : 666666667 calculated in 12500 ms
Result (Future): 666666667 calculated in 6922 ms
Wie so oft in dieser Art Blogbeitrag ist das Beispiel natürlich recht künstlich, aber hoffentlich wird trotzdem deutlich wie Future genutzt werden kann, um Tasks in Java parallel auszuführen. Und vielleicht wartet ja schon eine echte Aufgabe darauf mit Hilfe dieses Features gelöst zu werden.
Weitere Beiträge
von Thomas Jaspers
Dein Job bei codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
Gemeinsam bessere Projekte umsetzen.
Wir helfen deinem Unternehmen.
Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.
Hilf uns, noch besser zu werden.
Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.
Blog-Autor*in
Thomas Jaspers
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.