Cara Memulai Thread di Java

1. Perkenalan

Dalam tutorial ini, kita akan menjelajahi berbagai cara untuk memulai utas dan menjalankan tugas paralel.

Ini sangat berguna, khususnya saat menangani operasi yang panjang atau berulang yang tidak dapat berjalan di thread utama , atau di mana interaksi UI tidak dapat ditunda sambil menunggu hasil operasi.

Untuk mempelajari lebih lanjut tentang detail utas, baca tutorial kami tentang Siklus Hidup Utas di Java.

2. Dasar-dasar Menjalankan Thread

Kita dapat dengan mudah menulis beberapa logika yang berjalan di thread paralel dengan menggunakan framework Thread .

Mari kita coba contoh dasar, dengan memperluas kelas Thread :

public class NewThread extends Thread { public void run() { long startTime = System.currentTimeMillis(); int i = 0; while (true) { System.out.println(this.getName() + ": New Thread is running..." + i++); try { //Wait for one sec so it doesn't print too fast Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } ... } } }

Dan sekarang kami menulis kelas kedua untuk menginisialisasi dan memulai utas kami:

public class SingleThreadExample { public static void main(String[] args) { NewThread t = new NewThread(); t.start(); } }

Kita harus memanggil metode start () pada utas dalam status BARU (setara dengan tidak dimulai). Jika tidak, Java akan memunculkan instance pengecualian IllegalThreadStateException .

Sekarang mari kita asumsikan kita perlu memulai banyak utas:

public class MultipleThreadsExample { public static void main(String[] args) { NewThread t1 = new NewThread(); t1.setName("MyThread-1"); NewThread t2 = new NewThread(); t2.setName("MyThread-2"); t1.start(); t2.start(); } }

Kode kami masih terlihat cukup sederhana dan sangat mirip dengan contoh yang dapat kami temukan online.

Tentu saja, ini jauh dari kode siap produksi, di mana sangat penting untuk mengelola sumber daya dengan cara yang benar, untuk menghindari terlalu banyak pengalihan konteks atau terlalu banyak penggunaan memori.

Jadi, untuk mempersiapkan produksi kita sekarang perlu menulis boilerplate tambahan untuk menangani:

  • pembuatan thread baru yang konsisten
  • jumlah thread aktif yang berbarengan
  • threads deallocation: sangat penting bagi thread daemon untuk menghindari kebocoran

Jika mau, kita dapat menulis kode kita sendiri untuk semua skenario kasus ini dan bahkan lebih banyak lagi, tetapi mengapa kita harus menciptakan kembali roda?

3. Kerangka ExecutorService

The ExecutorService alat Thread Renang pola desain (juga disebut pekerja atau pekerja-kru direplikasi model) dan mengurus manajemen benang telah disebutkan di atas, ditambah menambahkan beberapa fitur yang sangat berguna seperti benang usabilitas dan tugas antrian.

Utas dapat digunakan kembali, khususnya, sangat penting: dalam aplikasi skala besar, mengalokasikan dan membatalkan alokasi banyak objek utas menciptakan overhead manajemen memori yang signifikan.

Dengan utas pekerja, kami meminimalkan biaya overhead yang disebabkan oleh pembuatan utas.

Untuk memudahkan konfigurasi kumpulan, ExecutorService hadir dengan konstruktor yang mudah dan beberapa opsi penyesuaian, seperti jenis antrian, jumlah minimum dan maksimum utas dan konvensi penamaannya.

Untuk detail lebih lanjut tentang ExecutorService, silakan baca Panduan kami untuk Java ExecutorService.

4. Memulai Tugas dengan Pelaksana

Berkat kerangka kerja yang kuat ini, kami dapat mengubah pola pikir kami dari memulai utas menjadi mengirimkan tugas.

Mari kita lihat bagaimana kita bisa mengirimkan tugas asinkron ke pelaksana kita:

ExecutorService executor = Executors.newFixedThreadPool(10); ... executor.submit(() -> { new Task(); });

Ada dua metode yang bisa kita gunakan: mengeksekusi , yang tidak mengembalikan apa-apa, dan mengirimkan , yang mengembalikan Future yang merangkum hasil komputasi.

Untuk informasi lebih lanjut tentang Futures, silakan baca Panduan kami untuk java.util.concurrent.Future.

5. Memulai Tugas dengan CompletableFutures

Untuk mengambil hasil akhir dari objek Future, kita dapat menggunakan metode get yang tersedia di objek, tetapi ini akan memblokir thread induk hingga akhir komputasi.

Alternatifnya, kita bisa menghindari pemblokiran dengan menambahkan lebih banyak logika ke tugas kita, tetapi kita harus meningkatkan kompleksitas kode kita.

Java 1.8 memperkenalkan kerangka kerja baru di atas konstruksi Future untuk bekerja lebih baik dengan hasil komputasi: CompletableFuture .

CompletableFuture mengimplementasikan CompletableStage , yang menambahkan banyak pilihan metode untuk melampirkan callback dan menghindari semua pipa ledeng yang diperlukan untuk menjalankan operasi pada hasil setelah siap.

Implementasi untuk mengirimkan tugas jauh lebih sederhana:

CompletableFuture.supplyAsync(() -> "Hello");

supplyAsync mengambil Pemasok yang berisi kode yang ingin kita jalankan secara asinkron - dalam kasus kita, parameter lambda.

Tugas sekarang secara implisit dikirimkan ke ForkJoinPool.commonPool () , atau kita bisa menentukan Pelaksana yang kita sukai sebagai parameter kedua.

Untuk mengetahui lebih lanjut tentang CompletableFuture, silakan baca Panduan Untuk CompletableFuture kami.

6. Menjalankan Tugas Tertunda atau Berkala

Saat bekerja dengan aplikasi web yang kompleks, kami mungkin perlu menjalankan tugas pada waktu tertentu, mungkin secara rutin.

Java memiliki beberapa alat yang dapat membantu kita menjalankan operasi yang tertunda atau berulang:

  • java.util.Timer
  • java.util.concurrent.ScheduledThreadPoolExecutor

6.1. Timer

Timer adalah fasilitas untuk menjadwalkan tugas untuk eksekusi di masa mendatang di thread latar belakang.

Tugas mungkin dijadwalkan untuk eksekusi satu kali, atau untuk eksekusi berulang secara berkala.

Mari kita lihat seperti apa kodenya jika kita ingin menjalankan tugas setelah satu detik penundaan:

TimerTask task = new TimerTask() { public void run() { System.out.println("Task performed on: " + new Date() + "n" + "Thread's name: " + Thread.currentThread().getName()); } }; Timer timer = new Timer("Timer"); long delay = 1000L; timer.schedule(task, delay);

Sekarang mari tambahkan jadwal berulang:

timer.scheduleAtFixedRate(repeatedTask, delay, period);

Kali ini, tugas akan berjalan setelah penundaan yang ditentukan dan akan berulang setelah jangka waktu berlalu.

Untuk informasi lebih lanjut, silakan baca panduan kami untuk Java Timer.

6.2. ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor has methods similar to the Timer class:

ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2); ScheduledFuture resultFuture = executorService.schedule(callableTask, 1, TimeUnit.SECONDS);

To end our example, we use scheduleAtFixedRate() for recurring tasks:

ScheduledFuture resultFuture = executorService.scheduleAtFixedRate(runnableTask, 100, 450, TimeUnit.MILLISECONDS);

The code above will execute a task after an initial delay of 100 milliseconds, and after that, it'll execute the same task every 450 milliseconds.

If the processor can't finish processing the task in time before the next occurrence, the ScheduledExecutorService will wait until the current task is completed, before starting the next.

To avoid this waiting time, we can use scheduleWithFixedDelay(), which, as described by its name, guarantees a fixed length delay between iterations of the task.

Untuk detail lebih lanjut tentang ScheduledExecutorService, silakan baca Panduan kami untuk Java ExecutorService.

6.3. Alat Mana yang Lebih Baik?

Jika kita menjalankan contoh di atas, maka hasil perhitungannya akan terlihat sama.

Jadi, bagaimana kita memilih alat yang tepat ?

Ketika kerangka kerja menawarkan banyak pilihan, penting untuk memahami teknologi yang mendasari untuk membuat keputusan yang tepat.

Mari kita coba menyelam lebih dalam di bawah tenda.

Timer :

  • tidak menawarkan jaminan real-time: tugas itu jadwal menggunakan Object.wait (panjang) metode
  • ada utas latar belakang tunggal, sehingga tugas berjalan secara berurutan dan tugas yang berjalan lama dapat menunda tugas lainnya
  • runtime exceptions thrown in a TimerTask would kill the only thread available, thus killing Timer

ScheduledThreadPoolExecutor:

  • can be configured with any number of threads
  • can take advantage of all available CPU cores
  • catches runtime exceptions and lets us handle them if we want to (by overriding afterExecute method from ThreadPoolExecutor)
  • cancels the task that threw the exception, while letting others continue to run
  • relies on the OS scheduling system to keep track of time zones, delays, solar time, etc.
  • provides collaborative API if we need coordination between multiple tasks, like waiting for the completion of all tasks submitted
  • provides better API for management of the thread life cycle

The choice now is obvious, right?

7. Difference Between Future and ScheduledFuture

In our code examples, we can observe that ScheduledThreadPoolExecutor returns a specific type of Future: ScheduledFuture.

ScheduledFuture extends both Future and Delayed interfaces, thus inheriting the additional method getDelay that returns the remaining delay associated with the current task. It's extended by RunnableScheduledFuture that adds a method to check if the task is periodic.

ScheduledThreadPoolExecutor mengimplementasikan semua konstruksi ini melalui kelas dalam ScheduledFutureTask dan menggunakannya untuk mengontrol siklus hidup tugas.

8. Kesimpulan

Dalam tutorial ini, kami bereksperimen dengan berbagai kerangka kerja yang tersedia untuk memulai utas dan menjalankan tugas secara paralel.

Kemudian, kami membahas lebih dalam perbedaan antara Timer dan ScheduledThreadPoolExecutor.

Kode sumber artikel tersedia di GitHub.