Perkenalan
Di
Bagian I , kami mengulas bagaimana utas dibuat. Mari kita ingat sekali lagi.
![Lebih baik bersama: Java dan kelas Thread. Bagian IV — Callable, Future, dan teman-teman - 1]()
Utas diwakili oleh kelas Utas, yang
run()
metodenya dipanggil. Jadi mari gunakan
compiler Java online Tutorialspoint dan jalankan kode berikut:
public class HelloWorld {
public static void main(String[] args) {
Runnable task = () -> {
System.out.println("Hello World");
};
new Thread(task).start();
}
}
Apakah ini satu-satunya pilihan untuk memulai tugas di utas?
java.util.concurrent.Callable
Ternyata
java.lang.Runnable memiliki saudara bernama
java.util.concurrent.Callable yang lahir di Java 1.5. Apa perbedaannya? Jika Anda mencermati Javadoc untuk antarmuka ini, kami melihat bahwa, tidak seperti
Runnable
, antarmuka baru mendeklarasikan
call()
metode yang mengembalikan hasil. Juga, itu melempar Pengecualian secara default. Artinya, ini menyelamatkan kita dari keharusan
try-catch
memblokir pengecualian yang diperiksa. Tidak buruk, bukan? Sekarang kami memiliki tugas baru alih-alih
Runnable
:
Callable task = () -> {
return "Hello, World!";
};
Tapi apa yang kita lakukan dengan itu? Mengapa kita membutuhkan tugas yang berjalan di utas yang mengembalikan hasil? Tentunya, untuk tindakan apa pun yang dilakukan di masa mendatang, kami berharap menerima hasil dari tindakan tersebut di masa mendatang. Dan kami memiliki antarmuka dengan nama yang sesuai:
java.util.concurrent.Future
java.util.concurrent.Future
Antarmuka
java.util.concurrent.Future mendefinisikan API untuk bekerja dengan tugas-tugas yang hasilnya akan kami terima di masa mendatang: metode untuk mendapatkan hasil, dan metode untuk memeriksa status. Sehubungan dengan
Future
, kami tertarik dengan penerapannya di kelas
java.util.concurrent.FutureTask . Ini adalah "Tugas" yang akan dijalankan di
Future
. Apa yang membuat implementasi ini lebih menarik adalah implementasinya juga Runnable. Anda dapat menganggap ini semacam adaptor antara model lama bekerja dengan tugas di utas dan model baru (baru dalam arti muncul di Java 1.5). Ini contohnya:
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class HelloWorld {
public static void main(String[] args) throws Exception {
Callable task = () -> {
return "Hello, World!";
};
FutureTask<String> future = new FutureTask<>(task);
new Thread(future).start();
System.out.println(future.get());
}
}
Seperti yang Anda lihat dari contoh, kami menggunakan
get
metode untuk mendapatkan hasil dari tugas.
Catatan:ketika Anda mendapatkan hasil menggunakan
get()
metode tersebut, eksekusi menjadi sinkron! Mekanisme apa yang menurut Anda akan digunakan di sini? Benar, tidak ada blok sinkronisasi. Itu sebabnya kami tidak akan melihat
WAITING di JVisualVM sebagai
monitor
atau
wait
, tetapi sebagai
park()
metode yang sudah dikenal (karena
LockSupport
mekanismenya sedang digunakan).
Antarmuka fungsional
Selanjutnya, kita akan berbicara tentang kelas-kelas dari Java 1.8, jadi sebaiknya kita memberikan pengantar singkat. Lihatlah kode berikut:
Supplier<String> supplier = new Supplier<String>() {
@Override
public String get() {
return "String";
}
};
Consumer<String> consumer = new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
};
Function<String, Integer> converter = new Function<String, Integer>() {
@Override
public Integer apply(String s) {
return Integer.valueOf(s);
}
};
Banyak dan banyak kode tambahan, bukan begitu? Setiap kelas yang dideklarasikan melakukan satu fungsi, tetapi kami menggunakan sekumpulan kode pendukung tambahan untuk mendefinisikannya. Dan begitulah pemikiran pengembang Java. Oleh karena itu, mereka memperkenalkan satu set "antarmuka fungsional" (
@FunctionalInterface
) dan memutuskan bahwa sekarang Java sendiri yang akan melakukan "pemikiran", hanya menyisakan hal-hal penting yang perlu kita khawatirkan:
Supplier<String> supplier = () -> "String";
Consumer<String> consumer = s -> System.out.println(s);
Function<String, Integer> converter = s -> Integer.valueOf(s);
Sebuah
Supplier
persediaan. Itu tidak memiliki parameter, tetapi mengembalikan sesuatu. Beginilah caranya memasok barang. A
Consumer
mengkonsumsi. Dibutuhkan sesuatu sebagai input (argumen) dan melakukan sesuatu dengannya. Argumennya adalah apa yang dikonsumsinya. Lalu kita juga punya
Function
. Dibutuhkan input (argumen), melakukan sesuatu, dan mengembalikan sesuatu. Anda dapat melihat bahwa kami secara aktif menggunakan obat generik. Jika Anda tidak yakin, Anda bisa mendapatkan penyegaran dengan membaca "
Generics in Java: how to use angled brackets in practice ".
Masa Depan Lengkap
Waktu berlalu dan kelas baru bernama
CompletableFuture
muncul di Java 1.8. Itu mengimplementasikan
Future
antarmuka, yaitu tugas kita akan selesai di masa mendatang, dan kita dapat menelepon
get()
untuk mendapatkan hasilnya. Tapi itu juga mengimplementasikan
CompletionStage
antarmuka. Namanya menjelaskan semuanya: ini adalah tahap tertentu dari serangkaian perhitungan. Pengantar singkat untuk topik ini dapat ditemukan dalam ulasan di sini: Pengantar Tahap Penyelesaian dan Masa Depan Lengkap. Mari kita langsung ke intinya. Mari kita lihat daftar metode statis yang tersedia yang akan membantu kita memulai:
![Lebih baik bersama: Java dan kelas Thread. Bagian IV — Callable, Future, dan teman-teman - 2]()
Berikut adalah opsi untuk menggunakannya:
import java.util.concurrent.CompletableFuture;
public class App {
public static void main(String[] args) throws Exception {
// A CompletableFuture that already contains a Result
CompletableFuture<String> completed;
completed = CompletableFuture.completedFuture("Just a value");
// A CompletableFuture that runs a new thread from Runnable. That's why it's Void
CompletableFuture<Void> voidCompletableFuture;
voidCompletableFuture = CompletableFuture.runAsync(() -> {
System.out.println("run " + Thread.currentThread().getName());
});
// A CompletableFuture that starts a new thread whose result we'll get from a Supplier
CompletableFuture<String> supplier;
supplier = CompletableFuture.supplyAsync(() -> {
System.out.println("supply " + Thread.currentThread().getName());
return "Value";
});
}
}
Jika kita menjalankan kode ini, kita akan melihat bahwa membuat
CompletableFuture
juga melibatkan peluncuran seluruh saluran pipa. Oleh karena itu, dengan kemiripan tertentu dengan SteamAPI dari Java8, di sinilah kami menemukan perbedaan antara pendekatan tersebut. Misalnya:
List<String> array = Arrays.asList("one", "two");
Stream<String> stringStream = array.stream().map(value -> {
System.out.println("Executed");
return value.toUpperCase();
});
Ini adalah contoh Stream API Java 8. Jika Anda menjalankan kode ini, Anda akan melihat bahwa "Dieksekusi" tidak akan ditampilkan. Dengan kata lain, saat aliran dibuat di Java, aliran tidak langsung dimulai. Sebaliknya, ia menunggu seseorang menginginkan nilai darinya. Tetapi
CompletableFuture
mulai mengeksekusi pipeline dengan segera, tanpa menunggu seseorang menanyakan nilainya. Saya pikir ini penting untuk dipahami. Jadi, kami memiliki file
CompletableFuture
. Bagaimana kita bisa membuat saluran pipa (atau rantai) dan mekanisme apa yang kita miliki? Ingat antarmuka fungsional yang kami tulis sebelumnya.
- Kami memiliki
Function
yang mengambil A dan mengembalikan B. Ini memiliki satu metode: apply()
.
- Kami memiliki
Consumer
yang mengambil A dan tidak mengembalikan apa pun (Void). Ini memiliki metode tunggal: accept()
.
- Kami memiliki
Runnable
, yang berjalan di utas, dan tidak mengambil apa pun dan tidak mengembalikan apa pun. Ini memiliki metode tunggal: run()
.
Hal berikutnya yang perlu diingat adalah yang
CompletableFuture
menggunakan
Runnable
,
Consumers
, dan
Functions
dalam pekerjaannya. Dengan demikian, Anda selalu dapat mengetahui bahwa Anda dapat melakukan hal berikut dengan
CompletableFuture
:
public static void main(String[] args) throws Exception {
AtomicLong longValue = new AtomicLong(0);
Runnable task = () -> longValue.set(new Date().getTime());
Function<Long, Date> dateConverter = (longvalue) -> new Date(longvalue);
Consumer<Date> printer = date -> {
System.out.println(date);
System.out.flush();
};
// CompletableFuture computation
CompletableFuture.runAsync(task)
.thenApply((v) -> longValue.get())
.thenApply(dateConverter)
.thenAccept(printer);
}
Metode
thenRun()
,
thenApply()
, dan
thenAccept()
memiliki versi "Async". Ini berarti bahwa tahapan ini akan diselesaikan pada utas yang berbeda. Utas ini akan diambil dari kumpulan khusus — jadi kami tidak akan tahu sebelumnya apakah itu utas baru atau lama. Itu semua tergantung pada seberapa intensif tugas-tugas itu. Selain metode tersebut, ada tiga kemungkinan yang lebih menarik. Untuk lebih jelasnya, bayangkan kita memiliki layanan tertentu yang menerima semacam pesan dari suatu tempat — dan ini membutuhkan waktu:
public static class NewsService {
public static String getMessage() {
try {
Thread.currentThread().sleep(3000);
return "Message";
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
}
Sekarang, mari kita lihat kemampuan lain yang
CompletableFuture
disediakan. Kita dapat menggabungkan hasil dari a
CompletableFuture
dengan hasil dari another
CompletableFuture
:
Supplier newsSupplier = () -> NewsService.getMessage();
CompletableFuture<String> reader = CompletableFuture.supplyAsync(newsSupplier);
CompletableFuture.completedFuture("!!")
.thenCombine(reader, (a, b) -> b + a)
.thenAccept(result -> System.out.println(result))
.get();
Perhatikan bahwa utas adalah utas daemon secara default, jadi untuk kejelasan, kami menggunakan
get()
untuk menunggu hasilnya. Kami tidak hanya dapat menggabungkan
CompletableFutures
, kami juga dapat mengembalikan a
CompletableFuture
:
CompletableFuture.completedFuture(2L)
.thenCompose((val) -> CompletableFuture.completedFuture(val + 2))
.thenAccept(result -> System.out.println(result));
Di sini saya ingin mencatat bahwa
CompletableFuture.completedFuture()
metode ini digunakan untuk singkatnya. Metode ini tidak membuat utas baru, sehingga sisa pipa akan dieksekusi pada utas yang sama di mana
completedFuture
ia dipanggil. Ada juga
thenAcceptBoth()
metode. Ini sangat mirip dengan
accept()
, tetapi jika
thenAccept()
menerima a
Consumer
,
thenAcceptBoth()
menerima
CompletableStage
+ lainnya
BiConsumer
sebagai input, yaitu a
consumer
yang membutuhkan 2 sumber, bukan satu. Ada kemampuan menarik lainnya yang ditawarkan oleh metode yang namanya menyertakan kata "Entah":
![Lebih baik bersama: Java dan kelas Thread. Bagian IV — Callable, Future, dan teman-teman - 3]()
Metode ini menerima alternatif
CompletableStage
dan dijalankan pada
CompletableStage
yang dieksekusi terlebih dahulu. Terakhir, saya ingin mengakhiri review ini dengan fitur menarik lainnya yaitu
CompletableFuture
: penanganan error.
CompletableFuture.completedFuture(2L)
.thenApply((a) -> {
throw new IllegalStateException("error");
}).thenApply((a) -> 3L)
//.exceptionally(ex -> 0L)
.thenAccept(val -> System.out.println(val));
Kode ini tidak akan melakukan apa-apa, karena akan ada pengecualian dan tidak ada lagi yang akan terjadi. Tetapi dengan menghapus komentar pada pernyataan "luar biasa", kami mendefinisikan perilaku yang diharapkan. Omong-omong
CompletableFuture
, saya juga menyarankan Anda untuk menonton video berikut:
Menurut pendapat saya yang sederhana, ini adalah salah satu video paling jelas di Internet. Mereka harus memperjelas cara kerja semua ini, perangkat apa yang kami miliki, dan mengapa semua ini diperlukan.
Kesimpulan
Mudah-mudahan, sekarang sudah jelas bagaimana Anda bisa menggunakan utas untuk mendapatkan perhitungan setelah selesai. Material tambahan:
Lebih baik bersama: Java dan kelas Thread. Bagian I — Utas eksekusi Lebih baik bersama: Java dan kelas Utas. Bagian II — Sinkronisasi Lebih baik bersama: Java dan kelas Thread. Bagian III — Interaksi Bersama yang lebih baik: Java dan kelas Thread. Bagian V — Pelaksana, ThreadPool, Fork/Bergabung Lebih baik bersama-sama: Java dan kelas Thread. Bagian VI — Tembak!
GO TO FULL VERSION