Asynchrones Arbeiten mit Java-Threads

Alle Android-Apps verwenden einen Hauptthread zur Verarbeitung von UI-Vorgängen. Anruf mit langer Ausführungszeit Vorgänge von diesem Hauptthread können zu einem Einfrieren und Nichtreagieren führen. Für Wenn Ihre Anwendung beispielsweise eine Netzwerkanfrage aus dem Hauptthread sendet, wird die Benutzeroberfläche der Anwendung ist eingefroren, bis die Netzwerkantwort eingeht. Wenn Sie Java verwenden, können Sie zusätzliche Hintergrundthreads erstellen, um lang andauernde Vorgänge verarbeitet der Hauptthread weiterhin UI-Aktualisierungen.

In diesem Leitfaden erfahren Sie, wie Entwickler, die die Programmiersprache Java verwenden, Thread-Pool, um mehrere Threads in einer Android-App einzurichten und zu verwenden. Dieser Leitfaden zeigt Ihnen auch, wie Sie Code zur Ausführung in einem Thread definieren und wie Sie zwischen einem dieser Threads und dem Hauptthread.

Gleichzeitigkeitsbibliotheken

Es ist wichtig, die Grundlagen von Threading Mechanismen. Es gibt jedoch viele beliebte Bibliotheken, die Konzepte und einsatzbereite Dienstprogramme zur Datenweitergabe zwischen Threads zu wechseln. Zu diesen Bibliotheken gehören Guava und RxJava für Nutzer der Java-Programmiersprache und Coroutines, die wir Kotlin-Nutzern empfehlen.

In der Praxis sollten Sie sich für die Methode entscheiden, die für Ihre App und Ihren Entwicklungsteam, obwohl die Regeln des Threading dieselben bleiben.

Beispiele – Übersicht

Die Beispiele in diesem Thema basieren auf dem Leitfaden zur App-Architektur. Netzwerkanfrage senden und das Ergebnis an den Hauptthread zurückgeben. wird dieses Ergebnis möglicherweise auf dem Bildschirm angezeigt.

Insbesondere ruft ViewModel die Datenschicht im Hauptthread auf, um die Netzwerkanfrage auslösen. Über die Datenschicht werden die Daten Ausführung der Netzwerkanfrage aus dem Hauptthread und Veröffentlichen des Ergebnisses über einen Callback mit dem Hauptthread verbunden.

Um die Ausführung der Netzwerkanfrage aus dem Hauptthread zu verschieben, müssen wir andere Threads in unserer App zu erstellen.

Mehrere Threads erstellen

Ein Thread-Pool ist eine verwaltete Sammlung von Threads, in denen Aufgaben ausgeführt werden. parallel zu einer Warteschlange. Neue Aufgaben werden auf vorhandenen Threads ausgeführt, und Threads werden inaktiv. Verwenden Sie zum Senden einer Aufgabe an einen Thread-Pool die Methode ExecutorService-Schnittstelle. ExecutorService hat nichts zu tun mit Services, der Android-Anwendungskomponente.

Das Erstellen von Threads ist teuer, daher sollten Sie einen Thread-Pool nur einmal erstellen, die App initialisiert wird. Denken Sie daran, die Instanz von ExecutorService zu speichern. entweder in Ihrer Application-Klasse oder in einem Abhängigkeitsinjektions-Container. Im folgenden Beispiel wird ein Thread-Pool aus vier Threads erstellt, mit dem wir Hintergrundaufgaben ausführen.

public class MyApplication extends Application {
    ExecutorService executorService = Executors.newFixedThreadPool(4);
}

Es gibt andere Möglichkeiten, einen Threadpool zu konfigurieren, je nach erwartetem Arbeitsbelastung. Weitere Informationen finden Sie unter Thread-Pool konfigurieren.

In einem Hintergrundthread ausführen

Eine Netzwerkanfrage an den Hauptthread führt dazu, dass der Thread wartet. block, bis eine Antwort eingeht. Da der Thread blockiert ist, kann das Betriebssystem onDraw() aufrufen und Ihre App bleibt hängen. Dies kann zu einer nicht Dialogfeld „Antwort (ANR)“. Führen wir diesen Vorgang stattdessen im Hintergrund aus. Diskussions-Thread.

Anfrage stellen

Sehen wir uns zuerst unseren LoginRepository-Kurs an. Netzwerkanfrage:

// Result.java
public abstract class Result<T> {
    private Result() {}

    public static final class Success<T> extends Result<T> {
        public T data;

        public Success(T data) {
            this.data = data;
        }
    }

    public static final class Error<T> extends Result<T> {
        public Exception exception;

        public Error(Exception exception) {
            this.exception = exception;
        }
    }
}

// LoginRepository.java
public class LoginRepository {

    private final String loginUrl = "https://example.com/login";
    private final LoginResponseParser responseParser;

    public LoginRepository(LoginResponseParser responseParser) {
        this.responseParser = responseParser;
    }

    public Result<LoginResponse> makeLoginRequest(String jsonBody) {
        try {
            URL url = new URL(loginUrl);
            HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection();
            httpConnection.setRequestMethod("POST");
            httpConnection.setRequestProperty("Content-Type", "application/json; charset=utf-8");
            httpConnection.setRequestProperty("Accept", "application/json");
            httpConnection.setDoOutput(true);
            httpConnection.getOutputStream().write(jsonBody.getBytes("utf-8"));

            LoginResponse loginResponse = responseParser.parse(httpConnection.getInputStream());
            return new Result.Success<LoginResponse>(loginResponse);
        } catch (Exception e) {
            return new Result.Error<LoginResponse>(e);
        }
    }
}

makeLoginRequest() ist synchron und blockiert den aufrufenden Thread. Um die Antwort der Netzwerkanfrage haben wir unsere eigene Result-Klasse.

Anfrage auslösen

Der ViewModel löst die Netzwerkanfrage aus, wenn der Nutzer beispielsweise auf eine Schaltfläche:

public class LoginViewModel {

    private final LoginRepository loginRepository;

    public LoginViewModel(LoginRepository loginRepository) {
        this.loginRepository = loginRepository;
    }

    public void makeLoginRequest(String username, String token) {
        String jsonBody = "{ username: \"" + username + "\", token: \"" + token + "\" }";
        loginRepository.makeLoginRequest(jsonBody);
    }
}

Mit dem vorherigen Code blockiert LoginViewModel den Hauptthread beim Erstellen der Netzwerkanfrage. Wir können den Thread-Pool, den wir instanziiert haben, zum Verschieben die Ausführung in einem Hintergrundthread.

Abhängigkeitsinjektion handhaben

Zuerst anhand der Prinzipien der Abhängigkeitsinjektion LoginRepository nimmt eine Instanz von Executor und nicht ExecutorService, Code ausführen und keine Threads verwalten:

public class LoginRepository {
    ...
    private final Executor executor;

    public LoginRepository(LoginResponseParser responseParser, Executor executor) {
        this.responseParser = responseParser;
        this.executor = executor;
    }
    ...
}

Die Methode execute() des Executors verwendet ein Runnable. Ein Runnable ist eine Schnittstelle für eine einzelne abstrakte Methode (Single Abstrakte Methode) mit einer run()-Methode, die ausgeführt wird in einen Thread aufrufen.

Im Hintergrund ausführen

Erstellen wir nun eine weitere Funktion namens makeLoginRequest(), im Hintergrundthread ausgeführt und die Antwort vorerst ignoriert:

public class LoginRepository {
    ...
    public void makeLoginRequest(final String jsonBody) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                Result<LoginResponse> ignoredResponse = makeSynchronousLoginRequest(jsonBody);
            }
        });
    }

    public Result<LoginResponse> makeSynchronousLoginRequest(String jsonBody) {
        ... // HttpURLConnection logic
    }
    ...
}

Innerhalb der Methode execute() erstellen wir eine neue Runnable mit dem Codeblock möchten wir im Hintergrund-Thread ausführen, in unserem Fall im synchronen -Anforderungsmethode festlegen. Intern verwaltet die ExecutorService die Runnable und in einem verfügbaren Thread ausgeführt wird.

Wissenswertes

Jeder Thread in Ihrer Anwendung kann parallel zu anderen Threads ausgeführt werden, einschließlich des Hauptthreads -Thread, Sie sollten also darauf achten, dass Ihr Code threadsicher ist. Beachten Sie, dass in der dass wir nicht in Variablen schreiben, die zwischen Threads gemeinsam genutzt werden, unveränderliche Daten. Dies ist eine bewährte Methode, da jeder Thread mit eine eigene Instanz von Daten erstellen und die Komplexität der Synchronisierung vermeiden.

Wenn Sie den Status zwischen Threads teilen müssen, müssen Sie bei der Zugriffsverwaltung vorsichtig sein aus Threads mithilfe von Synchronisierungsmechanismen wie Sperren. Dies ist außerhalb von den Umfang dieses Leitfadens. Im Allgemeinen sollten Sie die Freigabe veränderlicher Status vermeiden wenn möglich zwischen Threads wechseln.

Mit dem Hauptthread kommunizieren

Im vorherigen Schritt wurde die Antwort auf die Netzwerkanfrage ignoriert. Zum Anzeigen der Ergebnis auf dem Bildschirm angezeigt wird, muss LoginViewModel darüber informiert werden. Das können wir tun, indem wir mithilfe von Callbacks.

Die Funktion makeLoginRequest() sollte einen Callback als Parameter annehmen, sodass kann er einen Wert asynchron zurückgeben. Der Callback mit dem Ergebnis wird wenn die Netzwerkanfrage abgeschlossen wird oder ein Fehler auftritt. In Kotlin können wir eine übergeordnete Funktion. In Java müssen wir jedoch einen neuen Callback erstellen, mit der gleichen Funktionalität:

interface RepositoryCallback<T> {
    void onComplete(Result<T> result);
}

public class LoginRepository {
    ...
    public void makeLoginRequest(
        final String jsonBody,
        final RepositoryCallback<LoginResponse> callback
    ) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
                    callback.onComplete(result);
                } catch (Exception e) {
                    Result<LoginResponse> errorResult = new Result.Error<>(e);
                    callback.onComplete(errorResult);
                }
            }
        });
    }
  ...
}

ViewModel muss den Callback jetzt implementieren. Sie kann zu unterschiedlichen Logik, abhängig vom Ergebnis:

public class LoginViewModel {
    ...
    public void makeLoginRequest(String username, String token) {
        String jsonBody = "{ username: \"" + username + "\", token: \"" + token + "\" }";
        loginRepository.makeLoginRequest(jsonBody, new RepositoryCallback<LoginResponse>() {
            @Override
            public void onComplete(Result<LoginResponse> result) {
                if (result instanceof Result.Success) {
                    // Happy path
                } else {
                    // Show error in UI
                }
            }
        });
    }
}

In diesem Beispiel wird der Callback im aufrufenden Thread ausgeführt, der ein im Hintergrund. Das bedeutet, dass Sie keine Änderungen vornehmen oder direkt kommunizieren können. mit dem UI-Layer, bis Sie wieder zum Hauptthread wechseln.

Handler verwenden

Sie können einen Handler verwenden, um eine Aktion in die Warteschlange zu stellen, die an einem anderen Ort ausgeführt werden soll. Diskussions-Thread. Um den Thread anzugeben, auf dem die Aktion ausgeführt werden soll, erstellen Sie die Handler unter Verwendung eines Loopers für den Thread. Looper ist ein Objekt, das ausgeführt wird, die Nachrichtenschleife für einen verknüpften Thread. Nachdem Sie ein Handler erstellt haben, können Sie dann mit der Methode post(Runnable) einen Codeblock im zum entsprechenden Thread.

Looper enthält die Hilfsfunktion getMainLooper(), mit der die Looper des Hauptthreads. Mit diesem Befehl können Sie Code im Hauptthread ausführen: Looper, um ein Handler zu erstellen. Da dies oft vorkommt, können Sie auch eine Handler-Instanz an dem Ort speichern, an dem Sie die Datei gespeichert haben. ExecutorService:

public class MyApplication extends Application {
    ExecutorService executorService = Executors.newFixedThreadPool(4);
    Handler mainThreadHandler = HandlerCompat.createAsync(Looper.getMainLooper());
}

Es empfiehlt sich, den Handler in das Repository einzuschleusen, da dadurch mehr Flexibilität. Vielleicht möchten Sie in Zukunft zum Beispiel verschiedene Handler, um Aufgaben in einem separaten Thread zu planen. Wenn Sie immer wieder mit demselben Thread kommunizieren, können Sie das Handler an den Repository-Konstruktor wie im folgenden Beispiel gezeigt.

public class LoginRepository {
    ...
    private final Handler resultHandler;

    public LoginRepository(LoginResponseParser responseParser, Executor executor,
            Handler resultHandler) {
        this.responseParser = responseParser;
        this.executor = executor;
        this.resultHandler = resultHandler;
    }

    public void makeLoginRequest(
        final String jsonBody,
        final RepositoryCallback<LoginResponse> callback
    ) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
                    notifyResult(result, callback);
                } catch (Exception e) {
                    Result<LoginResponse> errorResult = new Result.Error<>(e);
                    notifyResult(errorResult, callback);
                }
            }
        });
    }

    private void notifyResult(
        final Result<LoginResponse> result,
        final RepositoryCallback<LoginResponse> callback,
    ) {
        resultHandler.post(new Runnable() {
            @Override
            public void run() {
                callback.onComplete(result);
            }
        });
    }
    ...
}

Wenn Sie mehr Flexibilität wünschen, können Sie auch ein Handler an jedes :

public class LoginRepository {
    ...

    public void makeLoginRequest(
        final String jsonBody,
        final RepositoryCallback<LoginResponse> callback,
        final Handler resultHandler,
    ) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
                    notifyResult(result, callback, resultHandler);
                } catch (Exception e) {
                    Result<LoginResponse> errorResult = new Result.Error<>(e);
                    notifyResult(errorResult, callback, resultHandler);
                }
            }
        });
    }

    private void notifyResult(
        final Result<LoginResponse> result,
        final RepositoryCallback<LoginResponse> callback,
        final Handler resultHandler
    ) {
        resultHandler.post(new Runnable() {
            @Override
            public void run() {
                callback.onComplete(result);
            }
        });
    }
}

In diesem Beispiel wurde der Callback an den makeLoginRequest des Repositorys übergeben. -Aufruf im Hauptthread ausgeführt wird. Das heißt, Sie können die Benutzeroberfläche direkt aus dem Callback aus oder verwende LiveData.setValue(), um mit der UI zu kommunizieren.

Threadpool konfigurieren

Sie können einen Threadpool mit einer der Hilfsfunktionen von Executor erstellen. mit vordefinierten Einstellungen, wie im vorherigen Beispielcode gezeigt. Alternativ können Sie Wenn Sie die Details des Thread-Pools anpassen möchten, können Sie einen mit ThreadPoolExecutor direkt aufrufen. Sie können Folgendes konfigurieren: Details:

  • Anfängliche und maximale Poolgröße.
  • Alive-Zeit und Zeiteinheit beibehalten. Die Keep-Alive-Zeit ist die maximale Dauer, kann der Thread inaktiv bleiben, bevor er heruntergefahren wird.
  • Eine Eingabewarteschlange, die Runnable Aufgaben enthält. Diese Warteschlange muss den Parameter BlockingQueue. So passen Sie die Anforderungen Ihrer App an: eine der verfügbaren Warteschlangenimplementierungen auswählen. Weitere Informationen findest du im Kurs Übersicht für ThreadPoolExecutor

Hier ist ein Beispiel, bei dem die Threadpoolgröße basierend auf der Gesamtzahl der Prozessorkerne, eine Keep-Alive-Zeit von einer Sekunde und eine Eingabewarteschlange.

public class MyApplication extends Application {
    /*
     * Gets the number of available cores
     * (not always the same as the maximum number of cores)
     */
    private static int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors();

    // Instantiates the queue of Runnables as a LinkedBlockingQueue
    private final BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>();

    // Sets the amount of time an idle thread waits before terminating
    private static final int KEEP_ALIVE_TIME = 1;
    // Sets the Time Unit to seconds
    private static final TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;

    // Creates a thread pool manager
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            NUMBER_OF_CORES,       // Initial pool size
            NUMBER_OF_CORES,       // Max pool size
            KEEP_ALIVE_TIME,
            KEEP_ALIVE_TIME_UNIT,
            workQueue
    );
    ...
}