Java Magazin   1.2014 - Spring IO

Erhältlich ab:  Dezember 2013

Autoren / Autorinnen: 
Frank Pientka ,  
Lars RöwekampArne Limburg ,  
Martin Lippert ,  
Oliver Heger ,  
Lars RöwekampArne Limburg ,  
Christian Pohl ,  
Dominik Obermaier ,  
Alexander Klein ,  
Kay Glahn ,  
Michael Müller ,  
Claudia Fröhling ,  
,  
Florian Pirchner ,  
Eberhard Wolff ,  
Gernot StarkePeter Hruschka ,  
Tam Hanna ,  
Christian MederBernhard Pflugfelder ,  
Josef AdersbergerJohannes SiederslebenJohannes Weigend ,  
Michael Hunger ,  
Florian Müller ,  
Konstantin DienerDaniel Kaminsky ,  
Sven Kölpin ,  
Oliver Gierke ,  
Dominik Obermaier ,  
Hartmut SchlosserDiana KupferClaudia Fröhling

„Die neue Spring-IO-Plattform ist für uns eine Zukunftsperspektive und hat mit dem klassischen ­Enterprise Java nicht mehr viel zu tun.“ Das erzählt mir Jürgen Höller, als ich ihn vor einigen Wochen auf der W-JAX in München zum Interview traf.

Es sind aufregende Zeiten für die Spring-Welt, denn man hat hart gearbeitet, um gleich mehrere wichtige Releases auf den Weg zu bringen:

  • eine neue Spring-Website

  • eine neue Spring-Plattform

  • das vierte Major-Release des Spring Frameworks

Neues Gewand

Auf der Hauskonferenz SpringOne in Santa Clara ging es los: Die Spring-Website hat ein neues Design spendiert bekommen und wurde auch neu strukturiert, mit vielen Getting Started Guides, die beim Einstieg in die Spring-Welt unterstützen sollen.

„Wir wollen einen völlig neuen Mehrwert mit der neuen Seite produzieren“, so Jürgen Höller. Die Seite soll aber nicht nur schöner aussehen, sie ist auch gleichzeitig die zukünftige Referenzanwendung für Spring 4. Denn die Seite selbst ist eine komplette Spring-Anwendung, aufbauend auf Spring 4 (RC), also State of the Art, wie man so schön sagt. Sobald der Code aufgeräumt und alle Fragen geklärt sind, soll die Seite sogar Open ­Source verfügbar sein. Hier ist natürlich besondere Vorsicht geboten, denn wer den Code kennt, hat theoretisch auch den Schlüssel für Angriffe gegen die Seite in der Hand.

Neuer Stack

Typesafe hat es mit seinem Scala-Stack bestehend aus der Sprache selbst, Akka, Play und weiteren Projekten vorgemacht, und Pivotal zieht jetzt nach: Alle Spring-Projekte werden jetzt unter dem Dach der neuen Spring-IO-Plattform zusammengefasst, mit stärkerem Middleware-Fokus und mit der eingangs zitierten Zukunftsperspektive. Die Plattform soll im ersten Quartal 2014 final sein.

Dabei finden sich im neuen Stack nicht nur die klassischen Spring-Projekte sondern auch ganz neue Familienmitglieder wie Spring XD, Reactor oder Spring Boot. Gerade Letzteres scheint der (heimliche?) Star der ganzen Unternehmung zu sein. Auch hier in unserem Heftschwerpunkt spielt er eine zentrale Rolle.

Neues Major-Release

Last but not least steht das Spring Framework, der Kern des ganzen Spring-Universums, kurz vor dem vierten großen Release seiner Geschichte. In dieser Version geht es nicht um Revolution, sondern um Evolution: „Spring 4 ist ein evolutionärer Schritt, weil es das Spring-3-Programmiermodell hernimmt, und angereichert und verfeinert wird. Spring 4 ist eine konsequente Fortsetzung des Spring-3-Modells“, erklärt Jürgen.

Wahrscheinlich spiegelt sich besonders in Spring 4 die Zukunftsperspektive wider, die für die ganze IT so wichtig ist: Spring 4 unterstützt nämlich unter anderem Technologien, die noch gar nicht verfügbar sind – zum Beispiel Java 8. Jürgen Höller ist sehr enthusiastisch, wenn es um die neuen Sprachfeatures geht: „Was in Java 8 kommt, ist sehr willkommen. Einige der neuen Sprachfeatures sind ein natürlicher Fit für die Spring-Welt. Spring Callbacks und Lambdas passen genau zusammen, haben dieselbe Konvention. Wir sehen es als unsere Aufgabe, diese Dinge zu promoten. Man sollte jetzt schon Java 8 für neue Projekte in Betracht ziehen.“

Top Notch also, und es ist ja auch verständlich: Wer sich Gedanken um die Zukunft macht, bleibt wettbewerbsfähig.

„Wir verstehen uns mit dem Spring Framework nicht nur als Laufzeitinfrastruktur, sondern auch als Development-Framework – und Development beginnt immer früher. Das heißt, wir sind stark orientiert an Early Adopters. Wir möchten Ihnen die Möglichkeit bieten, mit einem stabilen Spring-Framework-Release auch schon die nächste Generation der Technologien einsetzen zu können. Zum Beispiel bei einem länger laufenden Entwicklungsprojekt, das man heute startet. Aus meiner Sicht spricht nix dagegen, schon jetzt auf Technologien wie Spring 4 und Java 8 zu setzen.“

Sie haben Jürgen gehört – ran an die Arbeit! Und viel Spaß bei der Lektüre dieser Ausgabe.

froehling_claudia_sw_cropped.tif_fmt1.pngClaudia Fröhling, Redakteurin

Website Twitter Google Xing

In der letzten Folge haben Sie den Fahnder kennengelernt, der nach Architektur- oder Codesünden, technischen Schulden, Risiken und ähnlichen Missetaten in bestehenden Systemen sucht. Im zweiten Teil diskutieren wir, wie Fahnder mit den Ergebnissen dieser Suche weiterverfahren sollten.

Kennen Sie die Situation: Eine Ihnen bekannte Familie hat gemeinsam einen Urlaub in der Ferne verbracht. Nach deren Rückkehr fragen Sie Frau, Mann und Kinder getrennt nach ihren jeweiligen Eindrücken. Die jeweiligen Beschreibungen fallen total unterschiedlich aus, sodass Sie fast glauben können, die Befragten wären in Wirklichkeit an völlig verschiedenen Orten gewesen.

Subjektive Wahrnehmung lässt die aus Sicht einer einzelnen Person relevanten Eindrücke in den Vordergrund treten: Was einem Menschen als positive Eigenschaft besonders auffällt, bemerkt ein anderer erst gar nicht. Die Windsurferin wird den starken Wind ausführlich loben, ihr sonnenanbetender Partner sich über die ständige Brise beschweren (Ähnlichkeit zu mit den Autoren verwandten Personen ist beabsichtigt).

Lassen Sie uns zunächst mal zusammenfassen, was wir als Ergebnisse einer Fahndung nach Softwaresünden erwarten.

Fahndungsziel: Maßnahmenkatalog

Entgegen der kriminalistischen Fahndung geht es uns bei den Softwaresünden primär um Möglichkeiten, die gefundenen Probleme und Risiken möglichst einfach, kostengünstig und nachhaltig zu beseitigen: Wir möchten priorisierte und in Geld/Aufwandseinheiten bewertete Maßnahmen erarbeiten, mit denen sich die betreffende Software systematisch verbessern lässt.

Die in der letzten Folge beschriebenen Sünden (Architektur-, Code-, Prozess-, Management- und andere Sünden) dienen lediglich als Ausgangspunkt, solche konstruktiven Maßnahmen zu definieren. Nur in Ausnahmefällen möchten wir „Schuldige“ oder „Täter“ als solche identifizieren (um ihnen hoffentlich helfen zu können, aus ihren Fehlern zu lernen).

Bevor wir uns um Abhilfemaßnahmen kümmern können, müssen wir allerdings unsere gefundenen Spuren und Hinweise gründlich überprüfen, an ihnen rütteln und die Spreu vom Weizen trennen.

Sachdienliche Hinweise und Ablenkungsmanöver

Sie haben als Softwarefahnder unterschiedliche Aussagen und Spuren erhalten. Einige davon werden identische Sachverhalte betreffen („alle Befragten beschwerten sich über den zu langsamen CI-Server“), andere werden sich widersprechen (Zeuge A antwortet auf eine Frage mit Ja, Zeuge B auf dieselbe Frage mit Nein).

Manche Ihrer Zeugen lenken, bewusst oder unbewusst, die Aufmerksamkeit von eigenen Schwachstellen oder Fehlern ab: Kritische Probleme oder Risiken bezeichnen sie als Lappalien, bauschen aber Mücken zu Elefanten auf.

Eine Ihrer Aufgaben als gründlicher Fahnder besteht darin, solche reality distortions zu entlarven: Prüfen Sie Alibis, verifizieren Sie Zeugenaussagen durch gezielte Prüfungen des betroffenen Quellcodes oder anderer objektiver Informationsquellen.

System und Organisation

Softwaresysteme werden von Organisationen (Teams, Gruppen, Abteilungen) entwickelt und betreut. Während der Fahndung nach Softwaresünden vermischen Zeugen oftmals diese beiden Aspekte – und werden Ihnen sowohl organisatorische wie auch systemspezifische Probleme nennen.

Sie sollten das in Interviews durchaus zulassen, in Ihrer Nachbereitung allerdings säuberlich differenzieren. Organisatorische Probleme sollten Sie durch gezielte Fragen bei anderen Beteiligten verifizieren.

Manchmal bilden organisatorische Probleme die Ursache für vielfältige technische Schwierigkeiten: Wir haben beispielsweise erlebt, dass sich Organisationen nicht auf einheitliche Anforderungen und Randbedingungen für Systeme einigen konnten. Daher haben Teams dort an ein- und demselben System mit unterschiedlichen Vorgaben entwickelt – was langfristig natürlich zu gravierenden technischen Problemen führte.

Symptome auf Ursachen zurückführen

Versuchen Sie grundsätzlich, Symptome und Ursachen zu differenzieren. Anstatt dem Patienten immer weiter Schmerzmittel zu geben, sollten Sie besser die Dornen aus der Haut entfernen oder die schmerzhafte Entzündung kurieren.

Wir haben häufig eine fast manische Konzentration auf Symptome erlebt: Da werden Build- und CI-Server mit abstrusen Mengen an RAM versorgt, um den Build um einige Millisekunden zu beschleunigen ... statt Hunderte unnötiger Abhängigkeiten und überflüssiger Altlasten durch gezieltes Refactoring zu beseitigen (was den Build von Minuten auf Sekunden beschleunigt hätte).

Leider ist diese Ursachenforschung oft aufwändig und die Konzentration auf Symptome erfüllt das managementinhärente Bedürfnis nach „Wir wollen Taten sehen“ (aka Aktionismus) ... aber das ist eine andere Geschichte.

Probleme verursachen Kosten

Wir haben es in der letzten Folge „Follow the Money“ genannt – und möchten den finanziellen Aspekt von Softwaresünden erneut aufgreifen: Wenn ein Problem (eine technische Schuld, eine Softwaresünde) keine benennbaren Kosten verursacht, ist es kein Problem – zumindest aus Sicht von Budgetverantwortlichen.

Wir möchten verhindern, dass Sie aus rein akademischen Gründen, zum Selbstzweck oder der reinen Schönheit des Quellcodes zuliebe Änderungsmaßnahmen ergreifen. Jegliche Verbesserungs- oder Veränderungsmaßnahme kostet Geld/Aufwand und birgt gewisse Risiken – und diese Investition muss sich aus unserer Sicht auch finanziell lohnen. Klären Sie daher, welche Kosten durch die von Ihnen erkannten Probleme entstehen!

Negative Erfahrung hat uns gelehrt, dass manche „Zeugen“ oder betroffene Stakeholder sehr intensiv Probleme schildern, die nur sehr geringe Kosten nach sich ziehen (d. h. aus Gesamt- oder Managementsicht überhaupt keine relevanten Probleme darstellen).

Abhilfemaßnahmen definieren

Als Krönung Ihrer Fahndung schlagen Sie Maßnahmen vor, mit denen die erkannten Probleme, Sünden und Schulden behoben oder zumindest verbessert werden können. Dabei ist sowohl Ihr technischer Sachverstand wie auch Ihre Kreativität gefragt: Das bestehende Team besteht ja in der Regel aus klugen Menschen, die das Problem alleine nicht haben lösen können. Darum müssen Sie jetzt über den Tellerrand des „Normalen“ hinausdenken können.

Zusätzlich empfehlen wir Ihnen, Ihre Maßnahmen grundsätzlich durch folgende Zusatzinformationen zu fundieren:

  • Welche Kosten/Aufwände verursacht das Problem, das die jeweilige Maßnahme adressiert?

  • Welche Kosten/Aufwände verursacht die Maßnahme, also die Behebung?

  • Aus welchen Detailaufgaben besteht die Maßnahme? Wer oder welche Systemteile sind betroffen?

  • Welche Risiken birgt diese Maßnahme? Welche möglichen Konsequenzen drohen? (Beispiel: Sie steigern durch eine Maßnahme die Sicherheit des Systems auf Kosten der Benutzerfreundlichkeit.)

  • Welche Stakeholder und Ressourcen benötigen Sie zur Umsetzung der Maßnahme?

Oops – das ist eine schwierige Aufgabe: Die Maßnahmenvorschläge an sich werden Sie nach unserer Erfahrung recht schnell identifizieren können – aber deren Bewertung in Geld/Zeiteinheiten ist meist schwieriger. Ohne Letzteres wird Ihnen jedoch kaum ein Manager die Umsetzung von Maßnahmen erlauben.

Fazit

Hinterfragen und priorisieren Sie gefundene Probleme. Trennen Sie rein subjektive von objektiven („nachprüfbaren“) Problemen und fokussieren Sie (sich) auf diejenigen, die relevante Systemziele („Qualitätsanforderungen“) verletzen oder gefährden.

Organisatorische Probleme müssen Sie mit anderen Maßnahmen adressieren als rein technische Systemprobleme – aber: Auch Entwicklungs- oder Managementprozesse können Sie durch gezieltes Refactoring verbessern.

Wenn Sie Maßnahmen zur Verbesserung gefunden haben, dann bewerten Sie diese hinsichtlich ihres jeweiligen Nutzens und ihrer Kosten in Geld/Aufwandseinheiten – nur damit können Sie gegenüber Management und Budgetverantwortlichen nachhaltig argumentieren.

hruschka_starke_sw.tif_fmt2.pngPeter Hruschka (http://systemsguild.com) und Gernot Starke (innoQ-Fellow, http://gernotstarke.de) haben vor einigen Jahren www.arc42.de gegründet, das freie Portal für Softwarearchitekten. Sie sind Gründungsmitglieder des International Software Architecture Qualification Board (http://iSAQB.org) sowie Autoren mehrerer Bücher rund um Softwarearchitektur und Entwicklungsprozesse.

Twitter

In der Anfangszeit der Informatik war das Leben des Programmierers bequem: Jede neue Generation der zugrunde liegenden Hardware brachte eine wesentliche Performancesteigerung. Die immer effizienter werdenden Prozessorkerne führten die Routinen immer schneller aus, ohne dass von Seiten des Codierers Hilfe notwendig war. Leider ist die Fertigung von Halbleitern ein von diversen Naturgesetzen eingeschränkter Prozess.

Embedded Android

Mittlerweile sind die Strukturen im Desktopbereich so klein, dass schnellere Taktraten nur noch durch enorme Steigerung des Energieverbrauchs realisierbar sind. Chiphersteller reagieren auf diese geänderte Situation durch das Anbieten von Prozessoren, die immer mehr Kerne auf einmal mitbringen. Die Nutzung der dadurch gebotenen Mehrleistung ist leider alles andere als einfach, da sie auf Seiten der Informatiker ein Umdenken voraussetzt. Sega machte diese Erfahrung mit der Saturn: Die enorm leistungsfähige Konsole wurde nur von wenigen Entwicklern voll ausgelastet.

Reaktivität durch Parallelisierung

Im Mobile-Bereich war die Parallelisierung schon lange vor dem Aufkommen der ersten Mehrkernprozessoren ein brisantes Thema: Anders als am Desktop kommt es beim Handy aufgrund der viel kürzeren Sitzungen auf Reaktivität an. Das ist in mehrerlei Hinsicht kritisch: Erstens sind Handys an sich für schnelle Reaktionen nicht sonderlich geeignet. Das liegt unter anderem an der drahtlosen Funkverbindung, die bei umfangreicheren Netzwerkoperationen zu starken Latenzen führt.

Mobilbetriebssysteme realisieren ihre Benutzerschnittstelle meist in einem einzelnen, als GUI-Thread bezeichneten, Prozess. Er ist unter anderem für die Ausführung des Event Handlings zuständig: Die diversen Ereignisverarbeitungsroutinen laufen allesamt in ihm ab.

Wenn Ihr Programm nun in Reaktion auf einen Knopfdruck eine Netzwerkoperation beginnt, so bleibt im schlimmsten Fall der ganze Handheld stehen. Das ist aus Sicht der User höchst ärgerlich und wird mit schlechten Bewertungen im App Store quittiert.

Flucht aus dem GUI-Thread

Google animiert seine Entwickler durch den ANR-Dialog zum Erstellen von reaktiven Anwendungen. Er erscheint, wenn ein Programm auf einen Eingabebefehl fünf Sekunden lang nicht reagiert. Leider führt nicht jede Blocksituation zu einem ANR. In Listing 1 sehen Sie ein Code-Snippet, das ein Android-Smartphone lahmlegt – ein ANR-Dialog erscheint indes erst dann, wenn Sie auf einen anderen Button klicken.

Listing 1

public void onClick(View arg0) 
{
  if(arg0==myAnrButton)
  {
    int i=0;
    while(1==1)
    {
      i++;
      myView.setText(String.valueOf(i));
    }
  }
  
}

Dieses Programm verhält sich geradezu primitiv. Wird der Button angeklickt, so startet eine Endlosschleife. Diese verbrennt so lange Rechenleistung, bis der in Abbildung 1 gezeigte Dialog erscheint.

Besonders ärgerlich ist, dass der Nutzer die aktualisierten Informationen nie zu Gesicht bekommt. Der Button erscheint nach wie vor markiert, und die TextView aktualisiert sich nicht – wie sollte sie das auch tun, wenn sie keine Rechenleistung bekommt?

hanna_1.tif_fmt1.jpgAbb. 1: Der ANR-Dialog erlaubt dem Benutzer das sofortige Beenden des Programms

Beide Probleme lassen sich umgehen, indem Sie den Rechenprozess auf einen im Hintergrund rackernden ­Thread auslagern. In einer idealen Welt müssten Sie dazu nur den Code auslagern und mit einem Pointer auf das Formular versehen – der Rest ist die alleinige Aufgabe des Betriebssystems.

Leider ist das Erstellen eines threadsicheren GUI-Stacks alles andere als einfach. Aus diesem Grund umgehen die Betriebssystemhersteller dieses Problem nur allzu gern, indem sie einen GUI-Stack anbieten, der beim Aufruf einer Methode nur prüft, von wo aus die Invokation erfolgt. Ist der Aufrufer im falschen Thread beheimatet, so folgt eine Exception.

Um im Hintergrund arbeitenden Threads die Möglichkeit zur Aktualisierung des GUI zu geben, stellen die Entwickler zudem eine Spezialfunktion bereit. Diese nimmt ein Runnable entgegen, das danach am GUI-­Thread ausgeführt wird.

Mit diesen beiden Werkzeugen können Sie Ihre Applikation ohne Weiteres parallelisieren. Leider artet das in Arbeit aus, die von Drittanbietern gern vermieden wird. Zur Erleichterung offeriert Google den Background Worker. Dabei handelt es sich um eine Klasse, die den Gutteil der dazu notwendigen Logik mitbringt. Die Minimalversion der Klasse sieht wie in Listing 2 aus.

Listing 2

import android.os.AsyncTask;
public class MyAsyncTask extends AsyncTask<Params, Progress, Result> 
{
  protected Result doInBackground(Params... arg0) 
  {
    // TODO Auto-generated method stub
    return null;
  }
  
  protected void onPostExecute(Result arg0) 
  {
    // TODO Auto-generated method stub
  }
}

AsyncTask ist eine Templateklasse, die erst durch Vererbung ansprechbar wird. Die drei Parameter legen die Typen der an die Methoden übergebenen Werte fest. Params dient als Typ des Parameterarrays für die Arbeitsmethode doInBackground. Diese arbeitet die Jobs im Hintergrund ab und läuft nicht im GUI-Thread ab. Der zweite Parameter ist erst im nächsten Abschnitt von Bedeutung.

Nach dem erfolgreichen Abarbeiten von doInBackground folgt ein Aufruf von onPostExecute. Dieser erwartet als Parameter ein Objekt vom Typ Result, das am Ende von doInBackground zurückgegeben werden muss. OnPostExecute wird immer im GUI-Thread ausgeführt, darf also direkt auf die diversen Steuerelemente und Widgets zugreifen.

Das eigentliche Implementieren

Die Verwendung der resultierenden Klasse ist vergleichsweise einfach. Im GUI-Thread erstellen Sie eine neue Instanz der Klasse. Nach dem erfolgreichen Abarbeiten des Konstruktors setzen Sie eventuelle globale Member und rufen danach die execute-Methode auf. Ab diesem Zeitpunkt macht sich der AsyncTask selbsttätig an die Arbeit – die erfolgreiche Abarbeitung wird durch das Aufrufen der Methode onPostExecute angezeigt.

Mehr AsyncTask!

Beim Erstellen eines im Hintergrund ablaufenden Vorgangs ist es oft wünschenswert, den Nutzer über den Fortschritt der Operation zu informieren. AsyncTask bietet dafür eine eigene Methode an, die sich aus dem Arbeitsthread heraus aufrufen lässt. Als praktisches Beispiel dafür wollen wir einen AsyncTask erstellen, der im Hintergrund nutzlos herumrechnet und den Benutzer über eine TextView über seinen Zustand informiert. Sein Korpus sieht so aus:

public class MyAsyncTask extends AsyncTask<Integer, Float, String> 
{
  public TextView myView;

Von AsyncTask abgeleitete Klassen brauchen normalerweise keinen Konstruktor. Wenn Sie nur einen oder zwei Parameter übergeben möchte, so genügt es, diese als globale Variablen auszuführen – in unserem Fall reichen wir eine Referenz auf die in der Activity befindliche TextView in die Klasse. Die eigentliche Rechenarbeit erfolgt abermals in doInBackground (Listing 3).

Listing 3

@Override
protected String doInBackground(Integer... arg0) 
{
  for(int i=0;i<10000;i++)
  {
    if(i%100==0)
    {
      publishProgress((float)i/10000);
      
    }
    try {Thread.sleep(1);}
    catch (InterruptedException e) {}
  }
  return "Arbeit erledigt";
}

Im Vergleich zum Pseudocodebeispiel sticht hier die Verwendung der Methode publishProgress heraus. Sie erlaubt Ihnen, das Benutzerinterface über Fortschritte in der Abarbeitung des AsyncTasks zu informieren. Nach dem erfolgreichen Abrackern der Schleife wird ein Wert zurückgegeben, der von onPostExecute in die View geschrieben wird.

OnProgressUpdate wird immer dann aufgerufen, wenn publishProgress zur Weiterreichung von Statusinformationen angewiesen wird. In unserem Beispiel beschränkt sich die Funktion darauf, die anfallenden Daten 1:1 weiterzuschreiben:

protected void onProgressUpdate(Float... values) 
{
  myView.setText(values[0].toString());
}

Komplexere AsyncTasks verarbeiten oft Gruppen von Aufträgen. In diesem Fall dürfen Sie mehrere Parameter übergeben – ideal ist es, für jedes Auftragsobjekt ein Fortschrittsobjekt zu retournieren.

Beachten Sie, dass Fortschrittsberichte ebenfalls Rechenleistung in Anspruch nehmen. Wenn Sie den Benutzer zu oft informieren, verlängert sich die Gesamtrechnungsdauer. Das Verwenden eines Zählers im Zusammenspiel mit dem Modulo-Operator ist eine häufig angewandte Methode zur „Verwaltung“ der Aktualisierungshäufigkeit.

Abarbeitung mehrerer Anfragen?

Entwickler verwenden AsyncTasks normalerweise zur Auslagerung von Netzwerkzugriffen oder IO-Operationen. Zur Vereinfachung der Implementierung ließ Google in der Anfangszeit von Android alle AsyncTasks in einem Thread ablaufen – die Anfragen wurden nacheinander abgearbeitet.

Android-Versionen von 1.6 bis ausschließlich 3.0 nutzen stattdessen mehrere Threads, wodurch die sequenzielle Abarbeitung der doInBackground-Payloads nicht sichergestellt ist. Aufgrund diverser Probleme – laut Gerüchten waren sogar Teile des Betriebssystems auf diese Parallelität nicht vorbereitet – wurde das alte Verhalten ab Version 3.0 wieder eingeführt.

Wenn Sie Ihre AsyncTasks unter Android 3.0 parallel ausführen möchten, so dürfen Sie auf die Methode executeOnExecutor zurückgreifen. Diese nimmt eine Executor-Instanz entgegen, die die eigentliche Abarbeitung der Payload erledigt – die in der Klasse vordefinierte Konstante THREAD_POOL_EXECUTOR sorgt für parallelisierte Abarbeitung.

Geschwindigkeitsgewinn durch bessere Ressourcenauslastung

Die Verwendung eines BackgroundWorkers sorgt für eine bessere Reaktivität. Sofern Ihr Programm nicht mehrere Instanzen auf einmal loslässt, ist der Performancegewinn minimal: Die Arbeit läuft im Hintergrund ab, der GUI-Thread ist währenddessen arbeitslos.

Wenn Sie einen Algorithmus beschleunigen möchten, so müssen Sie ihn von Hand parallelisieren. Dabei sind durchaus beeindruckende Performancesteigerungen möglich: Arbeiten die beiden Instanzen voneinander unabhängig, so kann sich auf einem Doppelkernprozessor eine Verdoppelung der Performance ergeben. Die maximale Leistungssteigerung folgt dabei Amdahls Gesetz. Dieses sagt kurz gefasst aus, dass ein Programm auch mit unendlich vielen Prozessorkernen nicht unendlich stark beschleunigt werden kann. Die maximal erreichbare Geschwindigkeit ist davon abhängig, wie viel Zeit für die Ausführung des nicht parallelisierbaren Teils der Applikation erforderlich ist. Dieser Zusammenhang ist in Abbildung 2 zusammengefasst.

hanna_2.tif_fmt1.jpgAbb. 2: Amdahls Gesetz beschränkt den maximalen Rechenleistungsgewinn (Abbildung: WikiMedia Commons/Daniels220)

In diesem Zusammenhang darf ein weiterer Vorteil der Parallelisierung nicht unter den Tisch fallen. Manche Aufgaben setzen das Warten auf ein Ergebnis (z. B. das Eintreffen von Daten aus dem Remanentspeicher) voraus. Ein gutes Beispiel dafür ist die folgende Pseudocoderoutine:

void loadAndProc()
{
  res result = load(); //1000 ms delay
  return process (result); //1000 ms calc time
}

Das Parallelisieren dieser (zugegebenermaßen synthetischen) Aufgabe führt auch auf einem Telefon mit nur einem Kern zu einer wesentlichen Leistungssteigerung. Das liegt daran, dass zwei Threads ihre Arbeit im Idealfall exakt so aufteilen, dass stets einer wartet und der andere rechnet.

Threads von Hand

Unsere bisher besprochene AsyncTask-Klasse weist einige ärgerliche Schwächen auf. Neben dem nicht genau vorbestimmten Laufzeitverhalten ist sie immer an eine Activity gebunden – ein AsyncTask muss im GUI-­Thread „leben“ und stirbt normalerweise mit seiner Activity.

Für komplexere Rechenaufgaben empfiehlt sich die Verwendung der von Java bekannten Threadklasse. Diese besteht im Grunde genommen nur aus einer run()-Methode, die nach dem Aufrufen von start() in einem neuen Thread abgerackert wird. Wie schon beim Async­Task ist es auch hier ohne Weiteres möglich, dem ­Thread durch sein „Mutterobjekt“ weitere Informationen über die Ausführungsumgebung zukommen zu lassen.

Zum Aktualisieren der Steuerelemente müssen Sie beim Verwenden eines Threads auf Sondermethoden zurückgreifen. Diese nehmen normalerweise ein Runnable entgegen, das im GUI-Thread ausgeführt wird. Die mit Abstand am weitesten verbreitete ist die Funktion run­OnGuiThread, die Sie in jeder Activity finden – übergeben Sie ihr einfach ein Runnable, es wird dann am GUI-Thread ausgeführt.

Achtung, Kollisionsgefahr!

Solange die einzelnen Threads Ihres Programms nicht voneinander abhängig sind, treten normalerweise keine Probleme auf. Leider kommt es in der Praxis immer wieder zu so genannten Race Conditions, in denen mehrere Threads den Inhalt ein- und derselben Speicherstelle gegenseitig überschreiben und so zerstören.

Dieses Verhalten lässt sich am einfachsten anhand eines kleinen Beispiels erarbeiten. Der MyUnsafeThread ist eine absichtliche und besonders bösartige Implementierung, die das Problem besonders eindeutig demonstriert (Listing 4).

Listing 4

public class MyUnsafeThread extends Thread 
{
  MainActivity myMainActivity;
  public int myNumber;
  
  @Override
  public void run() 
  {
    while(1==1)
    {
      int counterCache=myMainActivity.myCounter;
      Log.d("com.tamoggemon.susmt", "Thread" + myNumber + "liest" + (counterCache));
      try {
        Thread.sleep(100);
      }
      catch (InterruptedException e) {
        e.printStackTrace();
      }
      myMainActivity.myCounter=counterCache+1;
      Log.d("com.tamoggemon.susmt", "Thread" + myNumber + "schreibt" + (counterCache+1));
    }
    
  }
}

Unser Thread beschafft sich den Wert des globalen Zählers, inkrementiert ihn und schreibt den geänderten Wert wieder zurück. Normalerweise würde dieser Prozess in „einem Rutsch“ ablaufen – auf einem Telefon mit einem Einkernprozessor wäre es sehr unwahrscheinlich, dass der Thread genau in der Mitte der drei Befehle unterbrochen wird. Aus diesem Grund fügen wir hier einen Delay ein, der für eine Verzögerung (und die Abarbeitung eines anderen Threads) sorgt. Losgelassen werden die Threads im Rahmen des Button-Handlers. Der dazu notwendige Code sieht so aus:

MyUnsafeThread[] someThreads=new MyUnsafeThread[5];
for(int i=0;i<5;i++)
{
  someThreads[i]=new MyUnsafeThread();
  someThreads[i].myNumber=i;
  someThreads[i].myMainActivity=this;
  someThreads[i].start();
  
}

An dieser Stelle ist eigentlich nur eine Sache wichtig: Sie dürfen zum Starten eines Java-Threads niemals seine run()-Methode aufrufen, da sonst kein neuer Thread angelegt wird – die richtige Funktion hört auf den Namen Start. Während der Programmausführung finden Sie in der Debuggerkonsole mehrere Zeilen nach dem folgenden Schema:

10-20 16:11:40.730: D/com.tamoggemon.susmt(11120): Thread 1 liest 1702
10-20 16:11:40.730: D/com.tamoggemon.susmt(11120): Thread 2 liest 1702
10-20 16:11:40.730: D/com.tamoggemon.susmt(11120): Thread 0 liest 1702
10-20 16:11:40.730: D/com.tamoggemon.susmt(11120): Thread 4 schreibt 1702
10-20 16:11:40.730: D/com.tamoggemon.susmt(11120): Thread 4 liest 1702

Hier lesen mehrere Threads die gleiche Information ein und schreiben sie erst mit wesentlicher Verzögerung zurück. Das führt dazu, dass viele Durchläufe des Zähl­threads unter den Tisch fallen – im obigen Beispiel würde Thread vier die von den drei vorhergehenden Threads gemachten Inkrementierungen beim Zurückschreiben seines veralteten internen Werts zunichtemachen.

Leider ist es in der Praxis oft weitaus schwieriger, Race Conditions zu detektieren. Das Auftreten dieses Fehlers ist nämlich von mehreren Umgebungsbedingungen abhängig: Neben der Anzahl der Prozessoren des Telefons hängt die Fehlerwahrscheinlichkeit auch davon ab, wie stark das System ausgelastet ist und wie der Scheduler die Threads im Speicher anordnet.

Synchronisiere mich!

Unser obiges Problem lässt sich lösen, indem wir die Ausführung des kritischen Teils des Programms immer nur einem Thread gleichzeitig erlauben. Java stellt auch dafür eine Lösung bereit, deren Nutzung nicht allzu schwierig ist (Listing 5).

Listing 5

synchronized (myMainActivity) 
{
  int counterCache=myMainActivity.myCounter;
  Log.d("com.tamoggemon.susmt", "Thread" + myNumber + "liest" + (counterCache));
  try {
    Thread.sleep(100);
  }
  catch (InterruptedException e) {
    e.printStackTrace();
  }
  myMainActivity.myCounter=counterCache+1;
  Log.d("com.tamoggemon.susmt", "Thread" + myNumber + "schreibt" + (counterCache+1));
}

synchronized-Blöcke verlangen ein Objekt, das als „Mutex“ gilt – es wird nicht weiter behelligt, muss aber während der gesamten Programmausführung konstant bleiben. Der im synchronized-Block befindliche Code darf immer nur von einem Thread gleichzeitig durchlaufen werden. Sobald ein Thread aktiv ist, müssen alle anderen Threads, die auf dasselbe Objekt synchronisiert sind, warten. In unserem Fall führt das abermals zu interessantem Verhalten. Die Ausgaben erfolgen nun ausschließlich aus dem ersten Thread, während die anderen nicht zum Zug kommen. Das liegt daran, dass die Thread­verarbeitung vom Betriebssystem immer während der Wartephase unterbrochen wird – da der 0-­Thread zu diesem Zeitpunkt noch im synchronized-Block steckt, sind die anderen Threads arbeitslos.

Synchronisation verursacht Overhead und Flaschenhälse. Unser oben gezeigtes Programm ist – trotz der Vielzahl der parallel arbeitenden Threads – auf die Performance eines einzelnen Prozessorkerns beschränkt. Das liegt daran, dass der synchronized-Block immer nur von einem Thread gleichzeitig betreten werden kann. Schläft dieser, so müssen alle anderen Threads warten.

Dies ist ein durchaus häufiges Problem in der parallelen Programmierung. Wenn die Teile der Gesamtaufgabe voneinander unabhängig sind, so empfiehlt sich das vorherige Aufteilen und nachherige Zusammenführen der Einzelelemente.

In unserem Fall würde es genügen, dass jeder Thread einen eigenen Zähler hat – nach dem Abarbeiten aller anstehenden Aufgaben fasst das Mutterprogramm die Ergebnisse zusammen. Wenn für die Verarbeitung größere Mengen an Quelldaten erforderlich sind, so ist es ratsam, diese vorher aufzuteilen – der Zugriff auf eine gemeinsame Datenstruktur kann unter Umständen auch einen Flaschenhals darstellen.

Kleine Sondertricks

Die Erstellung von Threads ist eine vergleichsweise teure Rechenoperation. Wenn Ihr Programm eine große Anzahl von trivialen Anfragen generiert, ist das Generieren der Klassen unter Umständen aufwändiger als die eigentliche Abarbeitung der Payload.

Dieses Problem lässt sich durch die Verwendung eines Threadpools umgehen. Dabei handelt es sich um eine Spezialklasse, die im Rahmen ihrer Erstellung eine Gruppe von Threads anlegt. Anstehende Arbeit wird in Runnables verpackt an die Threads verteilt, die diese danach abarbeiten. Zur Erleichterung der Kodierung stellt das Betriebssystem eine Gruppe von Hilfsfunktionen bereit, die unter [1] definiert sind.

Zur Kommunikation zwischen mehreren Threads empfiehlt sich die Verwendung eines Handlers. Dabei handelt es sich um eine Art Kanal zum Nachrichtenaustausch, der Ihnen das Hin- und Hersenden von Datenpaketen über Threadgrenzen hinaus ermöglicht.

Handler entstehen durch das Implementieren der abstrakten Handler-Mutterklasse, die eine Methode für den Nachrichteneingang bereitstellt. Diese wird aufgerufen, wenn ein Ereignis auftritt – der Inhalt der ausgetauschten Daten und die Reaktion liegt alleine im Ermessen des Programmierers.

Aufgrund der vergleichsweise effektiven Kommunikation werden Handler oft für die Kommunikation zwischen dem GUI-Thread und seinen Arbeitern eingesetzt. Weitere Informationen dazu finden Sie unter [2].

Fazit

Desktopprogrammierer sind im Moment noch in der Lage, der Parallelisierung ihres Codes aus dem Weg zu gehen. Im Mobil-Bereich sieht die Situation anders aus: Wer hier nicht parallelisiert, hat verloren. Das intelligente Aufteilen von Programmen ist ein faszinierendes Thema, das im akademischen Bereich seit Jahren für Neuerscheinungen und Promotionen sorgt. Für den durchschnittlichen Entwickler ist das wenig relevant: Die hier vorgestellten Codekonstrukte sollten 99 Prozent der in der Praxis notwendigen Einsatzfälle abdecken.

hanna_tam_sw.tif_fmt1.pngTam Hanna befasst sich seit der Zeit des Palm IIIc mit Programmierung und Anwendung von Handcomputern. Er entwickelt Programme für diverse Plattformen, betreibt Onlinenewsdienste zum Thema und steht für Fragen, Trainings und Vorträge gern zur Verfügung.

Mail