CodeGym /Java Blog /Acak /Lebih baik bersama: Java dan kelas Thread. Bagian IV — Ca...
John Squirrels
Level 41
San Francisco

Lebih baik bersama: Java dan kelas Thread. Bagian IV — Callable, Future, dan teman-teman

Dipublikasikan di grup Acak

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 - 1Utas 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-catchmemblokir 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 getmetode 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 monitoratau wait, tetapi sebagai park()metode yang sudah dikenal (karena LockSupportmekanismenya 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 Supplierpersediaan. Itu tidak memiliki parameter, tetapi mengembalikan sesuatu. Beginilah caranya memasok barang. A Consumermengkonsumsi. 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 CompletableFuturemuncul di Java 1.8. Itu mengimplementasikan Futureantarmuka, yaitu tugas kita akan selesai di masa mendatang, dan kita dapat menelepon get()untuk mendapatkan hasilnya. Tapi itu juga mengimplementasikan CompletionStageantarmuka. 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 - 2Berikut 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 CompletableFuturejuga 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 CompletableFuturemulai 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 Functionyang mengambil A dan mengembalikan B. Ini memiliki satu metode: apply().
  • Kami memiliki Consumeryang 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 CompletableFuturemenggunakan Runnable, Consumers, dan Functionsdalam 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 CompletableFuturedisediakan. Kita dapat menggabungkan hasil dari a CompletableFuturedengan 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 completedFutureia dipanggil. Ada juga thenAcceptBoth()metode. Ini sangat mirip dengan accept(), tetapi jika thenAccept()menerima a Consumer, thenAcceptBoth()menerima CompletableStage+ lainnya BiConsumersebagai input, yaitu a consumeryang 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 - 3Metode ini menerima alternatif CompletableStagedan dijalankan pada CompletableStageyang 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!
Komentar
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION