Pekerjaan asinkron dengan thread Java

Semua aplikasi Android menggunakan thread utama untuk menangani operasi UI. Memanggil berdurasi panjang operasi dari thread utama ini dapat menyebabkan berhenti berfungsi dan tidak responsif. Sebagai misalnya, jika aplikasi Anda membuat permintaan jaringan dari thread utama, maka UI aplikasi Anda dibekukan sampai menerima respons jaringan. Jika Anda menggunakan Java, Anda dapat membuat thread latar belakang tambahan untuk menangani operasi yang berjalan lama sekaligus thread utama terus menangani update UI.

Panduan ini menunjukkan bagaimana pengembang 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 untuk 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 perpustakaan populer yang menawarkan abstraksi terhadap konsep ini dan utilitas yang siap digunakan untuk meneruskan data antar-thread. Library ini mencakup Guava dan RxJava untuk pengguna dan Coroutine Bahasa Pemrograman Java, yang kami rekomendasikan untuk pengguna Kotlin.

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

Ringkasan contoh

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

Secara khusus, ViewModel memanggil lapisan data pada thread utama untuk memicu permintaan jaringan. Lapisan data bertugas 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 harus membuat thread lain di aplikasi kita.

Membuat beberapa thread

Kumpulan thread adalah kumpulan thread terkelola yang menjalankan tugas di paralel dari antrean. Tugas baru dijalankan pada thread yang ada sebagaimana dan thread menjadi tidak ada aktivitas. Untuk mengirim tugas ke kumpulan thread, gunakan metode Antarmuka ExecutorService. Perlu diketahui bahwa ExecutorService tidak ada hubungannya dengan Layanan, yaitu komponen aplikasi Android.

Pembuatan thread itu mahal, jadi sebaiknya Anda membuat kumpulan thread sekali saja aplikasi Anda melakukan inisialisasi. Pastikan untuk menyimpan instance ExecutorService baik di class Application atau di penampung injeksi dependensi. Contoh berikut membuat kumpulan thread yang terdiri 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 sebagian besar workload standar dan berbasis cloud. Lihat Mengonfigurasi kumpulan thread untuk mengetahui informasi selengkapnya.

Menjalankan eksekusi di thread latar belakang

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

Membuat Permintaan

Pertama, mari kita lihat class LoginRepository dan lihat cara pembuatannya 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, 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 terhadap permintaan jaringan. Kita dapat menggunakan kumpulan thread yang telah dibuat instance-nya untuk memindahkan eksekusinya 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 Abstrak Method (SAM) dengan metode run() yang dijalankan di sebuah thread saat dipanggil.

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

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

Pertimbangan

Setiap thread di aplikasi Anda dapat berjalan secara paralel dengan thread lainnya, termasuk thread utama , jadi Anda harus memastikan bahwa kode Anda aman untuk thread. Perhatikan bahwa di kita menghindari penulisan ke variabel yang dibagikan antar-thread, data yang tidak dapat diubah. Ini adalah praktik yang baik, karena setiap thread bekerja dengan instance data itu sendiri, dan kita menghindari kerumitan sinkronisasi.

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

Berkomunikasi dengan thread utama

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

Fungsi makeLoginRequest() harus mengambil callback sebagai parameter agar fungsi tersebut dapat menampilkan nilai secara asinkron. Callback dengan hasil dipanggil setiap kali permintaan jaringan selesai atau terjadi kegagalan. Di Kotlin, kita dapat menggunakan fungsi tingkat tinggi. Namun, di Java, kita harus membuat callback baru antarmuka agar memiliki fungsi 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 dalam thread panggilan, yang merupakan di thread latar belakang. Ini berarti Anda tidak dapat memodifikasi atau berkomunikasi secara langsung dengan lapisan UI hingga Anda beralih kembali ke thread utama.

Menggunakan pengendali

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

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

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

Ini adalah praktik yang baik untuk menginjeksikan pengendali ke dalam repositori, karena memberikan Anda lebih fleksibel. Misalnya, di masa mendatang Anda mungkin ingin meneruskan Handler yang berbeda untuk menjadwalkan tugas di thread terpisah. Jika Anda selalu berkomunikasi kembali ke thread yang sama, Anda dapat meneruskan Handler ke repositori repositori, seperti yang ditampilkan dalam 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 ingin lebih fleksibel, Anda dapat meneruskan Handler ke setiap {i>function<i}:

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. Itu berarti Anda dapat langsung memodifikasi UI dari callback atau gunakan 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 yang ditunjukkan dalam kode contoh sebelumnya. Sebagai alternatif, jika ingin menyesuaikan detail kumpulan thread, Anda dapat membuat instance yang menggunakan ThreadPoolExecutor secara langsung. Anda dapat mengonfigurasi detail:

  • Ukuran kumpulan awal dan maksimum.
  • Waktu keep alive dan satuan waktu. Waktu tetap aktif adalah durasi maksimum yang thread dapat tetap tidak ada aktivitas sebelum dimatikan.
  • Antrean input yang berisi tugas Runnable. Antrean ini harus mengimplementasikan BlockingQueue. Agar sesuai dengan persyaratan aplikasi, Anda dapat pilih dari implementasi antrean yang tersedia. Untuk mempelajari lebih lanjut, lihat kelas ringkasan untuk ThreadPoolExecutor.

Berikut contoh yang menentukan ukuran kumpulan thread berdasarkan jumlah total inti prosesor, waktu aktif selama 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
    );
    ...
}