Pekerjaan asinkron dengan thread Java

Semua aplikasi Android menggunakan thread utama untuk menangani operasi UI. Memanggil operasi yang berjalan lama dari thread utama ini dapat menyebabkan aplikasi berhenti berfungsi dan tidak responsif. Misalnya, jika aplikasi Anda membuat permintaan jaringan dari thread utama, UI aplikasi akan dibekukan hingga menerima respons jaringan. Jika menggunakan Java, Anda dapat membuat thread latar belakang tambahan untuk menangani operasi yang berjalan lama saat thread utama terus menangani update UI.

Panduan ini menunjukkan cara developer yang menggunakan Bahasa Pemrograman Java dapat menggunakan kumpulan thread untuk menyiapkan dan menggunakan beberapa thread di aplikasi Android. Panduan ini juga menunjukkan cara menentukan kode yang akan dijalankan di thread dan cara berkomunikasi antara salah satu thread ini dan thread utama.

Library serentak

Penting untuk memahami dasar-dasar threading dan mekanisme yang mendasarinya. Namun, ada banyak library populer yang menawarkan abstraksi tingkat lebih tinggi atas konsep ini dan utilitas yang siap digunakan untuk meneruskan data antar-thread. Library ini mencakup Guava dan RxJava untuk pengguna Bahasa Pemrograman Java dan Coroutine, yang kami rekomendasikan bagi pengguna Kotlin.

Dalam praktiknya, Anda harus memilih salah satu yang paling sesuai untuk aplikasi dan tim pengembangan, meskipun aturan threading tetap sama.

Ringkasan contoh

Berdasarkan Panduan arsitektur aplikasi, contoh dalam topik ini membuat permintaan jaringan dan menampilkan hasilnya ke thread utama, tempat aplikasi kemudian mungkin menampilkan hasilnya di layar.

Secara khusus, ViewModel memanggil lapisan data pada thread utama untuk memicu permintaan jaringan. Lapisan data bertanggung jawab memindahkan eksekusi permintaan jaringan dari thread utama dan memposting hasilnya kembali ke thread utama menggunakan callback.

Untuk memindahkan eksekusi permintaan jaringan dari thread utama, kita perlu membuat thread lain di aplikasi.

Membuat beberapa thread

Kumpulan thread adalah kumpulan thread terkelola yang menjalankan tugas secara paralel dari antrean. Tugas baru akan dijalankan pada thread yang ada saat thread tersebut tidak ada aktivitas. Untuk mengirim tugas ke kumpulan thread, gunakan antarmuka ExecutorService. Perlu diperhatikan bahwa ExecutorService tidak ada hubungannya dengan Layanan, komponen aplikasi Android.

Membuat thread itu mahal, jadi Anda harus membuat kumpulan thread hanya sekali saat aplikasi melakukan inisialisasi. Pastikan Anda menyimpan instance ExecutorService di class Application atau dalam penampung injeksi dependensi. Contoh berikut membuat kumpulan thread dari empat thread yang dapat kita gunakan untuk menjalankan tugas latar belakang.

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

Ada cara lain untuk mengonfigurasi kumpulan thread, bergantung pada beban kerja yang diharapkan. Lihat Mengonfigurasi kumpulan thread untuk informasi selengkapnya.

Menjalankan di thread latar belakang

Membuat permintaan jaringan di thread utama akan menyebabkan thread menunggu, atau memblokir, hingga thread tersebut menerima respons. Karena thread diblokir, OS tidak dapat memanggil onDraw(), dan aplikasi Anda berhenti berfungsi, yang berpotensi menyebabkan dialog Aplikasi Tidak Merespons (ANR). Sebagai gantinya, mari kita jalankan operasi ini di thread latar belakang.

Membuat Permintaan

Pertama, mari kita lihat class LoginRepository dan lihat caranya membuat permintaan jaringan:

// 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() bersifat sinkron dan memblokir thread pemanggil. Untuk membuat model respons permintaan jaringan, kita memiliki class Result sendiri.

Memicu permintaan

ViewModel memicu permintaan jaringan saat pengguna mengetuk, misalnya, sebuah tombol:

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);
    }
}

Dengan kode sebelumnya, LoginViewModel memblokir thread utama saat membuat permintaan jaringan. Kita dapat menggunakan kumpulan thread yang telah dibuat instance-nya untuk memindahkan eksekusi ke thread latar belakang.

Menangani injeksi dependensi

Pertama, mengikuti prinsip injeksi dependensi, LoginRepository mengambil instance Executor, bukan ExecutorService, karena mengeksekusi kode dan tidak mengelola thread:

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

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

Metode execute() Executor mengambil Runnable. Runnable adalah antarmuka Single Abstract Method (SAM) dengan metode run() yang dieksekusi di thread saat dipanggil.

Menjalankan di latar belakang

Mari kita buat fungsi lain bernama makeLoginRequest(), yang memindahkan eksekusi ke thread latar belakang dan mengabaikan respons untuk saat ini:

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
    }
    ...
}

Dalam metode execute(), kita membuat Runnable baru dengan blok kode yang ingin kita jalankan di thread latar belakang—dalam kasus kita, metode permintaan jaringan sinkron. Secara internal, ExecutorService mengelola Runnable dan mengeksekusinya dalam thread yang tersedia.

Pertimbangan

Setiap thread dalam aplikasi dapat berjalan secara paralel dengan thread lain, termasuk thread utama, jadi Anda harus memastikan bahwa kode Anda aman untuk thread. Perhatikan bahwa dalam contoh ini, kita menghindari penulisan ke variabel yang dibagikan antar-thread, dan meneruskan data yang tidak dapat diubah. Ini adalah praktik yang baik, karena setiap thread berfungsi dengan instance datanya sendiri, dan kita menghindari kompleksitas sinkronisasi.

Jika perlu berbagi status antar-thread, Anda harus berhati-hati dalam mengelola akses dari thread menggunakan mekanisme sinkronisasi seperti kunci. Hal ini berada di luar cakupan panduan ini. Secara umum, Anda harus menghindari berbagi status yang dapat diubah antar-thread jika memungkinkan.

Berkomunikasi dengan thread utama

Dalam langkah sebelumnya, kita mengabaikan respons permintaan jaringan. Untuk menampilkan hasil di layar, LoginViewModel perlu mengetahuinya. Kita dapat melakukannya menggunakan callback.

Fungsi makeLoginRequest() harus mengambil callback sebagai parameter agar dapat menampilkan nilai secara asinkron. Callback dengan hasilnya akan dipanggil setiap kali permintaan jaringan selesai atau terjadi kegagalan. Di Kotlin, kita dapat menggunakan fungsi tingkat tinggi. Namun, dalam Java, kita harus membuat antarmuka callback baru agar memiliki fungsionalitas yang sama:

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 perlu mengimplementasikan callback sekarang. Sistem ini dapat menjalankan logika yang berbeda, bergantung pada hasilnya:

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
                }
            }
        });
    }
}

Dalam contoh ini, callback dieksekusi di thread panggilan, yang merupakan thread latar belakang. Artinya, Anda tidak dapat memodifikasi atau berkomunikasi langsung dengan lapisan UI hingga Anda beralih kembali ke thread utama.

Menggunakan pengendali

Anda dapat menggunakan Handler untuk mengantrekan tindakan agar dilakukan pada thread lain. Untuk menentukan thread tempat menjalankan tindakan, buat Handler menggunakan Looper untuk thread. Looper adalah objek yang menjalankan loop pesan untuk thread terkait. Setelah membuat Handler, Anda kemudian dapat menggunakan metode post(Runnable) untuk menjalankan blok kode di thread yang sesuai.

Looper menyertakan fungsi bantuan, getMainLooper(), yang mengambil Looper thread utama. Anda dapat menjalankan kode di thread utama dengan menggunakan Looper ini untuk membuat Handler. Karena ini mungkin cukup sering Anda lakukan, Anda juga dapat menyimpan instance Handler di tempat yang sama dengan tempat Anda menyimpan ExecutorService:

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

Sebaiknya masukkan pengendali ke dalam repositori karena akan memberikan lebih banyak fleksibilitas. Misalnya, di masa mendatang, Anda mungkin ingin meneruskan Handler yang berbeda untuk menjadwalkan tugas di thread terpisah. Jika selalu berkomunikasi kembali ke thread yang sama, Anda dapat meneruskan Handler ke konstruktor repositori, seperti yang ditunjukkan pada contoh berikut.

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);
            }
        });
    }
    ...
}

Atau, jika menginginkan fleksibilitas lebih besar, Anda dapat meneruskan Handler ke setiap fungsi:

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);
            }
        });
    }
}

Dalam contoh ini, callback yang diteruskan ke panggilan makeLoginRequest Repositori akan dieksekusi di thread utama. Artinya, Anda dapat langsung memodifikasi UI dari callback atau menggunakan LiveData.setValue() untuk berkomunikasi dengan UI.

Mengonfigurasi kumpulan thread

Anda dapat membuat kumpulan thread menggunakan salah satu fungsi bantuan Executor dengan setelan yang telah ditentukan, seperti ditunjukkan dalam kode contoh sebelumnya. Atau, jika ingin menyesuaikan detail kumpulan thread, Anda dapat membuat instance menggunakan ThreadPoolExecutor secara langsung. Anda dapat mengonfigurasi detail berikut:

  • Ukuran kumpulan awal dan maksimum.
  • Waktu keep alive dan satuan waktu. Waktu keep alive adalah durasi maksimum thread yang dapat tetap tidak ada aktivitas sebelum dinonaktifkan.
  • Antrean input yang berisi tugas Runnable. Antrean ini harus menerapkan antarmuka BlockingQueue. Agar cocok dengan persyaratan aplikasi, Anda dapat memilih dari penerapan antrean yang tersedia. Untuk mempelajari lebih lanjut, lihat ringkasan class untuk ThreadPoolExecutor.

Berikut adalah contoh yang menentukan ukuran kumpulan thread berdasarkan jumlah total core prosesor, waktu keep alive satu detik, dan antrean input.

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
    );
    ...
}