Panduan untuk java.util.concurrent.Future

1. Gambaran keseluruhan

Dalam artikel ini, kita akan belajar tentang Masa Depan . Antaramuka yang ada sejak Java 1.5 dan sangat berguna ketika bekerja dengan panggilan tak segerak dan pemprosesan serentak.

2. Membuat Masa Depan

Ringkasnya, kelas Masa Depan mewakili hasil perhitungan tak segerak di masa depan - hasil yang akhirnya akan muncul di Masa Depan setelah proses selesai.

Mari lihat bagaimana menulis kaedah yang membuat dan mengembalikan contoh Masa Depan .

Kaedah jangka panjang adalah calon yang baik untuk pemprosesan tak segerak dan antara muka Masa Depan . Ini membolehkan kita melaksanakan beberapa proses lain sementara kita menunggu tugas yang dikemas dalam Masa Depan selesai.

Beberapa contoh operasi yang akan memanfaatkan sifat asinkron Future adalah:

  • proses intensif pengiraan (pengiraan matematik dan saintifik)
  • memanipulasi struktur data besar (data besar)
  • panggilan kaedah jarak jauh (memuat turun fail, pemecahan HTML, perkhidmatan web).

2.1. Melaksanakan Niaga Hadapan Dengan FutureTask

Sebagai contoh, kami akan membuat kelas yang sangat sederhana yang mengira segi empat sama bagi sebuah Integer . Ini jelas tidak sesuai dengan kategori kaedah "lama", tetapi kami akan membuat panggilan Thread.sleep () untuk membuatnya sehingga 1 saat terakhir selesai:

public class SquareCalculator { private ExecutorService executor = Executors.newSingleThreadExecutor(); public Future calculate(Integer input) { return executor.submit(() -> { Thread.sleep(1000); return input * input; }); } }

Bit kod yang benar-benar melakukan pengiraan terkandung dalam kaedah panggilan () , disediakan sebagai ungkapan lambda. Seperti yang anda lihat tidak ada yang istimewa mengenainya, kecuali panggilan tidur () yang disebutkan sebelumnya.

Ia menjadi lebih menarik apabila kita mengarahkan perhatian kita pada penggunaan Callable dan ExecutorService .

Callable adalah antara muka yang mewakili tugas yang mengembalikan hasil dan mempunyai kaedah panggilan tunggal () . Di sini, kami telah membuat contohnya menggunakan ungkapan lambda.

Membuat contoh Callable tidak membawa kita ke mana-mana, kita masih harus menyampaikan contoh ini kepada pelaksana yang akan menguruskan memulakan tugas itu dalam utas baru dan memberikan kita kembali objek Masa Depan yang berharga . Di situlah ExecutorService masuk.

Terdapat beberapa cara untuk mendapatkan contoh ExecutorService , kebanyakannya disediakan oleh kaedah kilang statik Executors . Dalam contoh ini, kami telah menggunakan newSingleThreadExecutor baru () , yang memberi kami ExecutorService yang mampu menangani satu utas pada satu masa.

Setelah kita mempunyai objek ExecutorService , kita hanya perlu memanggil submit () melewati Callable kita sebagai argumen. submit () akan menguruskan memulakan tugas dan mengembalikan objek FutureTask , yang merupakan implementasi antara muka Future .

3. Memakai Masa Depan

Hingga saat ini, kami telah belajar bagaimana membuat contoh Masa Depan .

Di bahagian ini, kita akan belajar bagaimana bekerja dengan contoh ini dengan meneroka semua kaedah yang merupakan bagian dari Future 's API.

3.1. Menggunakan isDone () dan get () untuk Mendapatkan Hasil

Sekarang kita perlu memanggil hitung () dan menggunakan Future yang dikembalikan untuk mendapatkan Integer yang dihasilkan . Dua kaedah dari Future API akan membantu kita dalam melaksanakan tugas ini.

Future.isDone () memberitahu kami jika pelaksana telah selesai memproses tugas. Sekiranya tugas itu selesai, ia akan kembali benar , sebaliknya akan menjadi salah .

Kaedah yang mengembalikan hasil sebenar dari pengiraan adalah Future.get () . Perhatikan bahawa kaedah ini menyekat pelaksanaan sehingga tugas selesai, tetapi dalam contoh kita, ini tidak akan menjadi masalah kerana kita akan memeriksa terlebih dahulu jika tugas selesai dengan memanggil isDone () .

Dengan menggunakan dua kaedah ini kita dapat menjalankan beberapa kod lain sementara kita menunggu tugas utama selesai:

Future future = new SquareCalculator().calculate(10); while(!future.isDone()) { System.out.println("Calculating..."); Thread.sleep(300); } Integer result = future.get();

Dalam contoh ini, kami menulis mesej ringkas mengenai output untuk memberitahu pengguna bahawa program sedang melakukan pengiraan.

Kaedah get () akan menyekat pelaksanaan sehingga tugas selesai. Tetapi kita tidak perlu risau kerana contoh kita hanya sampai ke titik di mana get () dipanggil setelah memastikan tugas selesai. Jadi, dalam senario ini, future.get () akan sentiasa kembali serta-merta.

Perlu disebutkan bahawa get () mempunyai versi yang terlalu banyak yang memerlukan timeout dan TimeUnit sebagai argumen:

Integer result = future.get(500, TimeUnit.MILLISECONDS);

Perbezaan antara get (long, TimeUnit) dan get () , adalah bahawa yang pertama akan membuang TimeoutException jika tugas tidak kembali sebelum jangka masa tamat yang ditentukan.

3.2. Membatalkan Future W engan membatalkan ()

Katakan kita telah mencetuskan tugas tetapi, untuk sebab tertentu, kita tidak peduli dengan hasilnya lagi. Kita boleh menggunakan Future.cancel (boolean) untuk memberitahu pelaksana untuk menghentikan operasi dan mengganggu urutan yang mendasari:

Future future = new SquareCalculator().calculate(4); boolean canceled = future.cancel(true);

Contoh Masa Depan kami dari kod di atas tidak akan pernah selesai beroperasi. Sebenarnya, jika kita berusaha memanggil get () dari contoh itu, setelah panggilan untuk membatalkan () , hasilnya akan menjadi PembatalanException . Future.isCancelled () akan memberitahu kami jika Masa Depan sudah dibatalkan. Ini sangat berguna untuk mengelakkan daripada mendapat PembatalanException .

Ada kemungkinan bahawa panggilan untuk membatalkan () gagal. Sekiranya demikian, nilai yang dikembalikan akan menjadi palsu . Perhatikan bahawa membatalkan () mengambil nilai boolean sebagai argumen - ini mengawal sama ada utas yang menjalankan tugas ini harus terganggu atau tidak.

4. Lebih Banyak Multithreading Dengan Thread Pools

ExecutorService kami sekarang adalah satu utas kerana ia diperoleh dengan Executors.newSingleThreadExecutor. Untuk menonjolkan "urutan tunggal" ini, mari kita mencetuskan dua pengiraan secara serentak:

SquareCalculator squareCalculator = new SquareCalculator(); Future future1 = squareCalculator.calculate(10); Future future2 = squareCalculator.calculate(100); while (!(future1.isDone() && future2.isDone())) { System.out.println( String.format( "future1 is %s and future2 is %s", future1.isDone() ? "done" : "not done", future2.isDone() ? "done" : "not done" ) ); Thread.sleep(300); } Integer result1 = future1.get(); Integer result2 = future2.get(); System.out.println(result1 + " and " + result2); squareCalculator.shutdown();

Sekarang mari kita menganalisis output untuk kod ini:

calculating square for: 10 future1 is not done and future2 is not done future1 is not done and future2 is not done future1 is not done and future2 is not done future1 is not done and future2 is not done calculating square for: 100 future1 is done and future2 is not done future1 is done and future2 is not done future1 is done and future2 is not done 100 and 10000

It is clear that the process is not parallel. Notice how the second task only starts once the first task is completed, making the whole process take around 2 seconds to finish.

To make our program really multi-threaded we should use a different flavor of ExecutorService. Let's see how the behavior of our example changes if we use a thread pool, provided by the factory method Executors.newFixedThreadPool():

public class SquareCalculator { private ExecutorService executor = Executors.newFixedThreadPool(2); //... }

With a simple change in our SquareCalculator class now we have an executor which is able to use 2 simultaneous threads.

If we run the exact same client code again, we'll get the following output:

calculating square for: 10 calculating square for: 100 future1 is not done and future2 is not done future1 is not done and future2 is not done future1 is not done and future2 is not done future1 is not done and future2 is not done 100 and 10000

This is looking much better now. Notice how the 2 tasks start and finish running simultaneously, and the whole process takes around 1 second to complete.

There are other factory methods that can be used to create thread pools, like Executors.newCachedThreadPool() that reuses previously used Threads when they are available, and Executors.newScheduledThreadPool() which schedules commands to run after a given delay.

For more information about ExecutorService, read our article dedicated to the subject.

5. Overview of ForkJoinTask

ForkJoinTask is an abstract class which implements Future and is capable of running a large number of tasks hosted by a small number of actual threads in ForkJoinPool.

In this section, we are going to quickly cover the main characteristics of ForkJoinPool. For a comprehensive guide about the topic, check our Guide to the Fork/Join Framework in Java.

Then the main characteristic of a ForkJoinTask is that it usually will spawn new subtasks as part of the work required to complete its main task. It generates new tasks by calling fork() and it gathers all results with join(), thus the name of the class.

There are two abstract classes that implement ForkJoinTask: RecursiveTask which returns a value upon completion, and RecursiveAction which doesn't return anything. As the names imply, those classes are to be used for recursive tasks, like for example file-system navigation or complex mathematical computation.

Let's expand our previous example to create a class that, given an Integer, will calculate the sum squares for all its factorial elements. So, for instance, if we pass the number 4 to our calculator, we should get the result from the sum of 4² + 3² + 2² + 1² which is 30.

First of all, we need to create a concrete implementation of RecursiveTask and implement its compute() method. This is where we'll write our business logic:

public class FactorialSquareCalculator extends RecursiveTask { private Integer n; public FactorialSquareCalculator(Integer n) { this.n = n; } @Override protected Integer compute() { if (n <= 1) { return n; } FactorialSquareCalculator calculator = new FactorialSquareCalculator(n - 1); calculator.fork(); return n * n + calculator.join(); } }

Notice how we achieve recursiveness by creating a new instance of FactorialSquareCalculator within compute(). By calling fork(), a non-blocking method, we ask ForkJoinPool to initiate the execution of this subtask.

The join() method will return the result from that calculation, to which we add the square of the number we are currently visiting.

Now we just need to create a ForkJoinPool to handle the execution and thread management:

ForkJoinPool forkJoinPool = new ForkJoinPool(); FactorialSquareCalculator calculator = new FactorialSquareCalculator(10); forkJoinPool.execute(calculator);

6. Conclusion

In this article, we had a comprehensive view of the Future interface, visiting all its methods. We've also learned how to leverage the power of thread pools to trigger multiple parallel operations. The main methods from the ForkJoinTask class, fork() and join() were briefly covered as well.

We have many other great articles on parallel and asynchronous operations in Java. Here are three of them that are closely related to the Future interface (some of them are already mentioned in the article):

  • Guide to CompletableFuture – an implementation of Future with many extra features introduced in Java 8
  • Panduan untuk Rangka Kerja Fork / Join di Java - lebih lanjut mengenai ForkJoinTask yang kami bahas di bahagian 5
  • Panduan untuk Java ExecutorService - khusus untuk antara muka ExecutorService

Lihat kod sumber yang digunakan dalam artikel ini di repositori GitHub kami.