Giới thiệu
Trong
Phần I , chúng ta đã xem xét cách tạo chuỗi. Hãy nhắc lại một lần nữa.
![Cùng nhau tốt hơn: Java và lớp Thread. Phần IV — Có thể gọi được, Tương lai và bạn bè - 1]()
Một luồng được đại diện bởi lớp Thread, có
run()
phương thức được gọi. Vì vậy, hãy sử dụng
trình biên dịch Java trực tuyến Tutorialspoint và thực thi đoạn mã sau:
public class HelloWorld {
public static void main(String[] args) {
Runnable task = () -> {
System.out.println("Hello World");
};
new Thread(task).start();
}
}
Đây có phải là tùy chọn duy nhất để bắt đầu một tác vụ trên chuỗi không?
java.util.concurrent.Callable
Hóa ra
java.lang.Runnable có một người anh em gọi là
java.util.concurrent.Callable , người đã ra đời trong Java 1.5. Sự khác biệt là gì? Nếu bạn xem kỹ Javadoc cho giao diện này, chúng ta sẽ thấy rằng, không giống như
Runnable
, giao diện mới khai báo một
call()
phương thức trả về kết quả. Ngoài ra, nó ném Ngoại lệ theo mặc định. Đó là, nó giúp chúng ta không phải
try-catch
chặn các ngoại lệ được kiểm tra. Không tệ, phải không? Bây giờ chúng tôi có một nhiệm vụ mới thay vì
Runnable
:
Callable task = () -> {
return "Hello, World!";
};
Nhưng chúng ta làm gì với nó? Tại sao chúng ta cần một tác vụ chạy trên một luồng trả về kết quả? Rõ ràng, đối với bất kỳ hành động nào được thực hiện trong tương lai, chúng tôi mong đợi nhận được kết quả của những hành động đó trong tương lai. Và chúng ta có một giao diện với tên tương ứng:
java.util.concurrent.Future
java.util.concurrent.Future
Giao diện
java.util.concurrent.Future xác định một API để làm việc với các tác vụ có kết quả mà chúng tôi dự định nhận trong tương lai: các phương thức để nhận kết quả và các phương thức để kiểm tra trạng thái. Liên quan đến
Future
, chúng tôi quan tâm đến việc triển khai nó trong lớp
java.util.concurrent.FutureTask . Đây là "Tác vụ" sẽ được thực thi trong
Future
. Điều làm cho việc triển khai này trở nên thú vị hơn nữa là nó cũng triển khai Runnable. Bạn có thể coi đây là một loại bộ điều hợp giữa mô hình cũ làm việc với các tác vụ trên luồng và mô hình mới (mới theo nghĩa nó đã xuất hiện trong Java 1.5). Đây là một ví dụ:
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());
}
}
Như bạn có thể thấy từ ví dụ, chúng tôi sử dụng
get
phương thức để lấy kết quả từ tác vụ.
Ghi chú:khi bạn nhận được kết quả bằng
get()
phương thức, việc thực thi sẽ trở nên đồng bộ! Bạn nghĩ cơ chế nào sẽ được sử dụng ở đây? Đúng, không có khối đồng bộ hóa. Đó là lý do tại sao chúng ta sẽ không thấy
WAITING trong JVisualVM là
monitor
hoặc
wait
mà là
park()
phương thức quen thuộc (vì
LockSupport
cơ chế này đang được sử dụng).
giao diện chức năng
Tiếp theo, chúng ta sẽ nói về các lớp từ Java 1.8, vì vậy chúng tôi sẽ cố gắng cung cấp một phần giới thiệu ngắn gọn. Nhìn vào đoạn mã sau:
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);
}
};
Bạn sẽ nói rất nhiều và rất nhiều mã bổ sung phải không? Mỗi lớp được khai báo thực hiện một chức năng, nhưng chúng tôi sử dụng một loạt mã hỗ trợ bổ sung để định nghĩa nó. Và đây là cách mà các nhà phát triển Java đã nghĩ. Theo đó, họ đã giới thiệu một tập hợp các "giao diện chức năng" (
@FunctionalInterface
) và quyết định rằng bây giờ chính Java sẽ thực hiện việc "suy nghĩ", chỉ để lại những thứ quan trọng để chúng ta lo lắng:
Supplier<String> supplier = () -> "String";
Consumer<String> consumer = s -> System.out.println(s);
Function<String, Integer> converter = s -> Integer.valueOf(s);
Một
Supplier
nguồn cung cấp. Nó không có tham số, nhưng nó trả về một cái gì đó. Đây là cách nó cung cấp mọi thứ. A
Consumer
tiêu thụ. Nó lấy thứ gì đó làm đầu vào (đối số) và thực hiện điều gì đó với nó. Đối số là những gì nó tiêu thụ. Sau đó, chúng tôi cũng có
Function
. Nó nhận đầu vào (đối số), thực hiện điều gì đó và trả về điều gì đó. Bạn có thể thấy rằng chúng tôi đang tích cực sử dụng thuốc generic. Nếu không chắc chắn, bạn có thể ôn lại bằng cách đọc "
Generics in Java: how to use the angled ngoặc trong thực tế ".
Tương lai hoàn thành
Thời gian trôi qua và một lớp mới có tên
CompletableFuture
đã xuất hiện trong Java 1.8. Nó triển khai
Future
giao diện, tức là các nhiệm vụ của chúng ta sẽ được hoàn thành trong tương lai và chúng ta có thể gọi
get()
để lấy kết quả. Nhưng nó cũng thực hiện
CompletionStage
giao diện. Cái tên nói lên tất cả: đây là một giai đoạn nhất định của một số bộ tính toán. Bạn có thể tìm thấy phần giới thiệu ngắn gọn về chủ đề trong phần đánh giá tại đây: Giới thiệu về Giai đoạn hoàn thành và Tương lai hoàn thành. Hãy đi thẳng vào vấn đề. Hãy xem danh sách các phương thức tĩnh có sẵn sẽ giúp chúng ta bắt đầu:
![Cùng nhau tốt hơn: Java và lớp Thread. Phần IV — Có thể gọi được, Tương lai và bạn bè - 2]()
Dưới đây là các tùy chọn để sử dụng chúng:
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";
});
}
}
Nếu chúng tôi thực thi mã này, chúng tôi sẽ thấy rằng việc tạo một
CompletableFuture
cũng liên quan đến việc khởi chạy toàn bộ quy trình. Do đó, với sự tương đồng nhất định với SteamAPI từ Java8, đây là nơi chúng tôi tìm thấy sự khác biệt giữa các cách tiếp cận này. Ví dụ:
List<String> array = Arrays.asList("one", "two");
Stream<String> stringStream = array.stream().map(value -> {
System.out.println("Executed");
return value.toUpperCase();
});
Đây là một ví dụ về Stream API của Java 8. Nếu bạn chạy mã này, bạn sẽ thấy rằng "Đã thực thi" sẽ không được hiển thị. Nói cách khác, khi một luồng được tạo trong Java, luồng đó không bắt đầu ngay lập tức. Thay vào đó, nó đợi ai đó muốn một giá trị từ nó. Nhưng
CompletableFuture
bắt đầu thực thi đường ống ngay lập tức mà không cần đợi ai đó yêu cầu giá trị. Tôi nghĩ rằng điều này là quan trọng để hiểu. Vì vậy, chúng tôi có một tệp
CompletableFuture
. Làm thế nào chúng ta có thể tạo một đường ống dẫn (hoặc chuỗi) và chúng ta có những cơ chế nào? Nhớ lại những giao diện chức năng mà chúng ta đã viết trước đó.
- Chúng ta có a
Function
lấy A và trả về B. Nó có một phương thức duy nhất: apply()
.
- Chúng tôi có a
Consumer
nhận A và không trả lại gì (Void). Nó có một phương pháp duy nhất: accept()
.
- Chúng tôi có
Runnable
, chạy trên luồng và không lấy gì và không trả lại gì. Nó có một phương pháp duy nhất: run()
.
Điều tiếp theo cần nhớ là
CompletableFuture
sử dụng
Runnable
,
Consumers
, và
Functions
trong công việc của nó. Theo đó, bạn luôn có thể biết rằng bạn có thể thực hiện những việc sau với
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);
}
Các phương thức
thenRun()
,
thenApply()
và
thenAccept()
có phiên bản "Không đồng bộ". Điều này có nghĩa là các giai đoạn này sẽ được hoàn thành trên một luồng khác. Chủ đề này sẽ được lấy từ một nhóm đặc biệt — vì vậy chúng tôi sẽ không biết trước đó sẽ là chủ đề mới hay cũ. Tất cả phụ thuộc vào mức độ tính toán chuyên sâu của các nhiệm vụ. Ngoài những phương pháp này, có ba khả năng thú vị hơn. Để rõ ràng, hãy tưởng tượng rằng chúng ta có một dịch vụ nhất định nhận một số loại tin nhắn từ đâu đó — và điều này cần có thời gian:
public static class NewsService {
public static String getMessage() {
try {
Thread.currentThread().sleep(3000);
return "Message";
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
}
Bây giờ, chúng ta hãy xem các khả năng khác
CompletableFuture
cung cấp. Chúng ta có thể kết hợp kết quả của a
CompletableFuture
với kết quả của other
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();
Lưu ý rằng các luồng là luồng daemon theo mặc định, vì vậy để rõ ràng, chúng tôi sử dụng
get()
để chờ kết quả. Chúng ta không chỉ có thể kết hợp
CompletableFutures
mà còn có thể trả về một
CompletableFuture
:
CompletableFuture.completedFuture(2L)
.thenCompose((val) -> CompletableFuture.completedFuture(val + 2))
.thenAccept(result -> System.out.println(result));
Ở đây tôi muốn lưu ý rằng
CompletableFuture.completedFuture()
phương pháp này đã được sử dụng cho ngắn gọn. Phương pháp này không tạo một luồng mới, vì vậy phần còn lại của đường ống sẽ được thực thi trên cùng một luồng
completedFuture
được gọi. Ngoài ra còn có một
thenAcceptBoth()
phương pháp. Nó rất giống với
accept()
, nhưng nếu
thenAccept()
chấp nhận a
Consumer
, thì chấp nhận +
thenAcceptBoth()
khác làm đầu vào, tức là a lấy 2 nguồn thay vì một. Có một khả năng thú vị khác được cung cấp bởi các phương thức có tên bao gồm từ "Hoặc": Các phương thức này chấp nhận một phương án thay thế và được thực thi trên phương thức được thực hiện trước. Cuối cùng, tôi muốn kết thúc bài đánh giá này bằng một tính năng thú vị khác của : xử lý lỗi.
CompletableStage
BiConsumer
consumer
![Cùng nhau tốt hơn: Java và lớp Thread. Phần IV — Có thể gọi được, Tương lai và bạn bè - 3]()
CompletableStage
CompletableStage
CompletableFuture
CompletableFuture.completedFuture(2L)
.thenApply((a) -> {
throw new IllegalStateException("error");
}).thenApply((a) -> 3L)
//.exceptionally(ex -> 0L)
.thenAccept(val -> System.out.println(val));
Mã này sẽ không làm gì cả, bởi vì sẽ có một ngoại lệ và sẽ không có gì khác xảy ra. Nhưng bằng cách bỏ ghi chú câu lệnh "ngoại lệ", chúng tôi xác định hành vi dự kiến. Nói về
CompletableFuture
, tôi cũng khuyên bạn nên xem video sau:
Theo ý kiến khiêm tốn của tôi, đây là một trong những video giải thích hay nhất trên Internet. Họ nên làm rõ cách thức hoạt động của tất cả những thứ này, chúng tôi có sẵn bộ công cụ nào và tại sao tất cả những thứ này lại cần thiết.
Phần kết luận
Hy vọng rằng bây giờ đã rõ ràng về cách bạn có thể sử dụng các chuỗi để nhận các phép tính sau khi hoàn thành. Tài liệu bổ sung:
Kết hợp tốt hơn: Java và lớp Thread. Phần I - Các luồng thực thi Cùng nhau tốt hơn: Java và lớp Thread. Phần II — Đồng bộ hóa Tốt hơn khi kết hợp với nhau: Java và lớp Thread. Phần III — Tương tác tốt hơn khi kết hợp với nhau: Java và lớp Thread. Phần V — Executor, ThreadPool, Fork/Join Together tốt hơn: Java và lớp Thread. Phần VI — Bắn đi!
GO TO FULL VERSION