JavaRush /Blog Java /Random-VI /Bạn không thể hủy hoại Java chỉ bằng một luồng: Phần IV -...
Viacheslav
Mức độ

Bạn không thể hủy hoại Java chỉ bằng một luồng: Phần IV - Có thể gọi được, Tương lai và những người bạn

Xuất bản trong nhóm

Giới thiệu

Chúng ta đã xem xét cách tạo chủ đề trong phần đầu tiên . Chúng ta hãy nhớ lại. Bạn không thể làm hỏng Java bằng một chủ đề: Phần IV - Có thể gọi được, Tương lai và những người bạn - 1Một luồng là Threadthứ gì đó chạy trong đó run, vì vậy hãy sử dụng trình biên dịch trực tuyến java 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 để chạy một tác vụ trong một chuỗi không?

java.util.concurrent.Callable

Hóa ra java.lang.Runnable có một người anh trai tên là java.util.concurrent.Callable và anh ấy sinh ra ở Java 1.5. Sự khác biệt là gì? Nếu chúng ta xem xét kỹ hơn JavaDoc của 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 phương thức calltrả về một kết quả. Ngoài ra, theo mặc định, nó sẽ ném Ngoại lệ. Nghĩa là, nó giúp chúng ta không cần phải viết try-catchcác khối cho các ngoại lệ được kiểm tra. Cũng không tệ lắm phải không? Bây giờ chúng ta có Runnablemột nhiệm vụ mới thay thế:
Callable task = () -> {
	return "Hello, World!";
};
Nhưng phải làm gì với nó? Tại sao chúng ta lại cần một tác vụ chạy trên một luồng trả về kết quả? Rõ ràng, trong tương lai chúng ta mong đợi nhận được kết quả của những hành động sẽ được thực hiện trong tương lai. Tương Lai trong tiếng Anh – Future. Và có một giao diện có cùng tên:java.util.concurrent.Future

java.util.concurrent.Future

Giao diện java.util.concurrent.Future mô tả một API để làm việc với các tác vụ mà chúng tôi dự định đạt được kết quả trong tương lai: phương pháp lấy kết quả, phương pháp kiểm tra trạng thái. Chúng tôi Futurequan tâm đến việc triển khai java.util.concurrent.FutureTask của nó . Nghĩa là Task, đây là những gì sẽ được thực thi trong Future. Điều thú vị về việc triển khai này là nó triển khai và Runnable. Bạn có thể coi đây là một loại bộ chuyển đổi của mô hình cũ để làm việc với các tác vụ trong luồng và mô hình mới (mới theo nghĩa là 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ư có thể thấy từ ví dụ, sử dụng phương pháp này chúng ta thu được getkết quả từ bài toán task. (!)Quan trọng, tại thời điểm kết quả thu được bằng phương thức này, getviệc thực thi sẽ trở nên đồng bộ. Bạn nghĩ cơ chế nào sẽ được sử dụng ở đây? Đúng vậy, không có khối đồng bộ hóa - do đó chúng ta sẽ thấy WAITING trong JVisualVM không phải là monitoror wait, mà là chính khối đó park(vì cơ chế được sử dụng LockSupport).

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 sẽ rất hữu ích nếu giới thiệu ngắn gọn. Chúng ta hãy xem đ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);
	}
};
Có rất nhiều mã không cần thiết phải không? Mỗi lớp được khai báo thực hiện một chức năng duy nhất, nhưng để mô tả nó, chúng ta sử dụng một loạt mã phụ trợ không cần thiết. Và các nhà phát triển Java cũng nghĩ như vậy. Do đó, họ đã giới thiệu một bộ “giao diện chức năng” ( @FunctionalInterface) và quyết định rằng bây giờ chính Java sẽ “nghĩ ra” mọi thứ cho chúng ta, ngoại trừ những thứ quan trọng:
Supplier<String> supplier = () -> "String";
Consumer<String> consumer = s -> System.out.println(s);
Function<String, Integer> converter = s -> Integer.valueOf(s);
Supplier- các nhà cung cấp. Nó không có tham số, nhưng nó trả về một cái gì đó, tức là nó cung cấp cho nó. Consumer- người tiêu dùng. Nó lấy thứ gì đó làm đầu vào (tham số s) và thực hiện điều gì đó với nó, tức là tiêu thụ thứ gì đó. Có một chức năng khác. Nó lấy một cái gì đó làm đầu vào (tham số s), thực hiện một cái gì đó và trả về một cái gì đó. Như chúng ta thấy, thuốc generic được sử dụng tích cực. Nếu không chắc chắn, bạn có thể ghi nhớ chúng và đọc “ Lý thuyết về generics trong Java hoặc cách đặt dấu ngoặc đơn trong thực tế ”.

Hoàn thànhTương lai

Thời gian trôi qua, Java 1.8 đã giới thiệu một lớp mới gọi là CompletableFuture. Nó triển khai giao diện Future, nghĩa là giao diện của chúng ta tasksẽ được thực thi trong tương lai và chúng ta có thể thực thi getvà nhận được kết quả. Nhưng anh ấy cũng thực hiện một số CompletionStage. Từ bản dịch, mục đích của nó đã rõ ràng: đó là một Giai đoạn nhất định của một loại tính toán nào đó. Bạn có thể tìm thấy phần giới thiệu ngắn gọn về chủ đề này trong phần tổng quan " Giới thiệu về CompleteionStage và CompleteableFuture ". 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 để giúp chúng ta bắt đầu: Bạn không thể làm hỏng Java bằng một chủ đề: Phần IV - Có thể gọi được, Tương lai và những người bạn - 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 {
        // CompletableFuture уже содержащий результат
        CompletableFuture<String> completed;
        completed = CompletableFuture.completedFuture("Просто meaning");
        // CompletableFuture, запускающий (run) новый поток с Runnable, поэтому он Void
        CompletableFuture<Void> voidCompletableFuture;
        voidCompletableFuture = CompletableFuture.runAsync(() -> {
            System.out.println("run " + Thread.currentThread().getName());
        });
        // CompletableFuture, запускающий новый поток, результат которого возьмём у Supplier
        CompletableFuture<String> supplier;
        supplier = CompletableFuture.supplyAsync(() -> {
            System.out.println("supply " + Thread.currentThread().getName());
            return "Значение";
        });
    }
}
Nếu chạy mã này, chúng ta sẽ thấy việc tạo đó CompletableFutureliên quan đến việc bắt đầu toàn bộ chuỗi. Do đó, mặc dù có một số điểm tương đồng với SteamAPI từ Java8, nhưng đây chính là điểm 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ề Java 8 Stream Api (bạn có thể đọc thêm về nó tại đây " Hướng dẫn API luồng Java 8 trong hình ảnh và ví dụ "). Nếu bạn chạy mã này, nó Executedsẽ không hiển thị. Nghĩa là, khi tạo một luồng trong Java, luồng đó không bắt đầu ngay lập tức mà đợi cho đến khi cần một giá trị từ luồng đó. Nhưng CompletableFuturenó bắt đầu thực thi chuỗi ngay lập tức mà không cần đợi nó được yêu cầu về giá trị được tính toán. Tôi nghĩ điều quan trọng là phải hiểu điều này. Vậy là chúng ta có CompleteableFuture. Làm thế nào chúng ta có thể tạo ra một chuỗi và chúng ta có phương tiện gì? Hãy nhớ lại các giao diện chức năng mà chúng ta đã viết trước đó.
  • Chúng ta có một hàm ( Function) nhận vào A và trả về B. Nó có một phương thức duy nhất - apply(áp dụng).
  • Chúng ta có một người tiêu dùng ( Consumer) chấp nhận A và không trả lại gì ( Void ). Nó chỉ có một phương pháp - accept(chấp nhận).
  • Chúng tôi có mã chạy trên một chuỗi Runnablekhông chấp nhận hoặc trả lại. Nó có một phương thức duy nhất - run(chạy).
Điều thứ hai cần nhớ là CompletalbeFuturetrong công việc của mình, nó sử dụng Runnablengười tiêu dùng và chức năng. Vì điều này, bạn luôn có thể nhớ rằng bạn CompletableFuturecó thể làm điều này:
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);
}
Phương thức thenRunthenApplyphiên bản . thenAccept_ AsyncĐiều này có nghĩa là các giai đoạn này sẽ được thực thi trong một luồng mới. Nó sẽ được lấy từ một hồ bơi đặc biệt nên không biết trước nó sẽ có dòng chảy như thế nào, mới hay cũ. Tất cả phụ thuộc vào độ khó của nhiệm vụ. Ngoài những phương pháp này, còn 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 tin nhắn từ đâu đó và 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ờ, hãy xem xét các tính năng khác của CompletableFuture. Chúng ta có thể kết hợp kết quả này CompletableFuturevới kết quả của một kết quả khác 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();
Điều đáng chú ý là theo mặc định, các luồng sẽ là các luồng daemon, vì vậy để rõ ràng get, chúng ta sử dụng để chờ kết quả. Và chúng ta không chỉ có thể kết hợp (kết hợp) mà còn có thể return 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 để cho ngắn gọn, phương pháp này đã được sử dụng CompletableFuture.completedFuture. Phương thức này không tạo ra một luồng mới, do đó phần còn lại của chuỗi sẽ được thực thi trong cùng một luồng mà nó được gọi completedFuture. Ngoài ra còn có một phương pháp thenAcceptBoth. Nó rất giống với accept, nhưng nếu thenAcceptnó chấp nhận consumer, thì thenAcceptBothnó chấp nhận một CompletableStage+ khác làm đầu vào BiConsumer, tức là consumerchấp nhận 2 nguồn làm đầu vào chứ không phải một. Có một khả năng thú vị khác với từ này Either: Bạn không thể hủy hoại Java chỉ bằng một sợi dây: Phần IV - Có thể gọi được, Tương lai và những người bạn - 3Các phương thức này chấp nhận một phương án thay thế CompletableStagevà sẽ được thực thi trên phương thức CompletableStageđược thực thi trước. Và 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 CompletableFuture- xử lý lỗi.
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ì... một ngoại lệ sẽ được ném ra và sẽ không có gì xảy ra. Nhưng nếu chúng ta bỏ ghi chú exceptionallythì chúng ta sẽ xác định hành vi. CompletableFutureTôi cũng khuyên bạn nên xem video sau về chủ đề này : Theo quan điểm khiêm tốn của tôi, những video này là một trong những video trực quan nhất trên Internet. Họ phải nói rõ tất cả hoạt động như thế nào, chúng tôi có kho vũ khí gì và tại sao tất cả đều cần thiết.

Phần kết luận

Hy vọng bây giờ bạn đã rõ cách sử dụng các luồng để truy xuất các phép tính sau khi chúng được tính toán. Tài liệu bổ sung: #Viacheslav
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION