CodeGym /Blog Java /Ngẫu nhiên /Cùng nhau tốt hơn: Java và lớp Thread. Phần IV - Có thể g...
John Squirrels
Mức độ
San Francisco

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è

Xuất bản trong nhóm

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è - 1Mộ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-catchchặ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 getphươ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à monitorhoặc waitmà là park()phương thức quen thuộc (vì LockSupportcơ 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 Suppliernguồ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 Consumertiê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 Futuregiao 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 CompletionStagegiao 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è - 2Dướ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 CompletableFuturecũ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 CompletableFuturebắ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 Functionlấy A và trả về B. Nó có một phương thức duy nhất: apply().
  • Chúng tôi có a Consumernhậ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à CompletableFuturesử dụng Runnable, Consumers, và Functionstrong 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()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 CompletableFuturecung cấp. Chúng ta có thể kết hợp kết quả của a CompletableFuturevớ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 CompletableFuturesmà 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. CompletableStageBiConsumerconsumerCù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è - 3CompletableStageCompletableStageCompletableFuture

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!
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION