CodeGym/Java Blog/무작위의/더 나은 조합: Java와 Thread 클래스. 4부 — Callable, Future 및 친구
John Squirrels
레벨 41
San Francisco

더 나은 조합: Java와 Thread 클래스. 4부 — Callable, Future 및 친구

무작위의 그룹에 게시되었습니다
회원

소개

1부 에서는 스레드가 생성되는 방법을 검토했습니다. 한 번 더 기억해 봅시다. 스레드는 메소드가 호출되는 더 나은 조합: Java와 Thread 클래스.  4부 — Callable, Future 및 친구 - 1Thread 클래스로 표시됩니다 . Tutorialspoint 온라인 Java 컴파일러를run() 사용 하고 다음 코드를 실행해 보겠습니다.
public class HelloWorld {

    public static void main(String[] args) {
        Runnable task = () -> {
            System.out.println("Hello World");
        };
        new Thread(task).start();
    }
}
이것이 스레드에서 작업을 시작하는 유일한 옵션입니까?

java.util.concurrent.Callable

알고 보니 java.lang.Runnable에는 Java 1.5에서 탄생한 java.util.concurrent.Callable 이라는 형제가 있습니다 . 차이점은 무엇입니까? 이 인터페이스에 대한 Javadoc을 자세히 살펴보면 와 달리 Runnable새 인터페이스가 call()결과를 반환하는 메서드를 선언한다는 것을 알 수 있습니다. 또한 기본적으로 예외가 발생합니다. 즉, try-catch확인된 예외를 차단하지 않아도 됩니다. 나쁘지 않죠? 이제 다음 대신 새 작업이 있습니다 Runnable.
Callable task = () -> {
	return "Hello, World!";
};
그러나 우리는 그것으로 무엇을합니까? 결과를 반환하는 스레드에서 실행되는 작업이 필요한 이유는 무엇입니까? 분명히 미래에 수행되는 모든 작업에 대해 미래에 해당 작업의 결과를 받을 것으로 기대합니다. 그리고 해당 이름을 가진 인터페이스가 있습니다.java.util.concurrent.Future

java.util.concurrent.Future

java.util.concurrent.Future 인터페이스는 미래에 결과를 받을 계획인 작업(결과를 가져오는 메서드 및 상태를 확인하는 메서드) 작업을 위한 API를 정의합니다 . 와 관련하여 java.util.concurrent.FutureTaskFuture 클래스 의 구현에 관심이 있습니다 . 이것은 에서 실행될 "작업"입니다 . 이 구현을 더욱 흥미롭게 만드는 것은 Runnable도 구현한다는 것입니다. 이것은 스레드에서 작업하는 이전 모델과 새 모델(Java 1.5에 등장했다는 점에서 새 모델) 사이의 일종의 어댑터라고 생각할 수 있습니다. 다음은 예입니다. Future
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());
    }
}
예제에서 볼 수 있듯이 get메소드를 사용하여 태스크에서 결과를 가져옵니다. 메모:메서드 를 사용하여 결과를 얻으면 get()실행이 동기화됩니다! 여기에 어떤 메커니즘이 사용될 것이라고 생각하십니까? 사실, 동기화 블록이 없습니다. 그렇기 때문에 JVisualVM에서 WAITING을 a monitor또는 로 보지 않고 wait친숙한 park()방법으로 볼 수 있습니다( LockSupport메커니즘이 사용되고 있기 때문).

기능적 인터페이스

다음으로 Java 1.8의 클래스에 대해 이야기할 것이므로 간략한 소개를 제공하는 것이 좋습니다. 다음 코드를 살펴보십시오.
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);
	}
};
아주 많은 추가 코드가 있지 않습니까? 선언된 각 클래스는 하나의 기능을 수행하지만 이를 정의하기 위해 여러 추가 지원 코드를 사용합니다. 이것이 Java 개발자가 생각한 방식입니다. 따라서 그들은 일련의 "기능적 인터페이스"( @FunctionalInterface)를 도입하고 이제 Java 자체가 "사고"를 수행하고 우리가 걱정할 중요한 사항만 남겨두기로 결정했습니다.
Supplier<String> supplier = () -> "String";
Consumer<String> consumer = s -> System.out.println(s);
Function<String, Integer> converter = s -> Integer.valueOf(s);
공급 Supplier합니다. 매개변수는 없지만 무언가를 반환합니다. 이것이 물건을 공급하는 방법입니다. A는 Consumer소모한다. 입력(인수)으로 무언가를 취하고 그것으로 무언가를 수행합니다. 논쟁은 그것이 소비하는 것입니다. 그런 다음 우리는 또한 있습니다 Function. 입력(인수)을 받고, 무언가를 수행하고, 무언가를 반환합니다. 제네릭을 적극적으로 사용하고 있음을 알 수 있습니다. 확실하지 않은 경우 " Java의 Generics: how to use 꺾쇠 괄호를 실제로 사용하는 방법 "을 읽어 복습할 수 있습니다.

CompletableFuture

CompletableFuture시간이 흐르고 Java 1.8에서 라는 새로운 클래스가 등장했습니다. 그것은 Future인터페이스를 구현합니다. 즉, 우리의 작업은 미래에 완료될 것이고 우리는 get()결과를 얻기 위해 호출할 수 있습니다. 그러나 인터페이스도 구현합니다 CompletionStage. 이름에서 모든 것을 알 수 있습니다. 이것은 일부 계산 세트의 특정 단계입니다. 주제에 대한 간략한 소개는 CompletionStage 및 CompletableFuture 소개 리뷰에서 찾을 수 있습니다. 본론으로 들어가겠습니다. 시작하는 데 도움이 되는 사용 가능한 정적 메서드 목록을 살펴보겠습니다. 더 나은 조합: Java와 Thread 클래스.  4부 — Callable, Future 및 친구 - 2사용 옵션은 다음과 같습니다.
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";
        });
    }
}
이 코드를 실행하면 CompletableFuture전체 파이프라인을 시작하는 것도 포함된다는 것을 알 수 있습니다. 따라서 Java8의 SteamAPI와 어느 정도 유사하므로 여기에서 이러한 접근 방식의 차이점을 찾을 수 있습니다. 예를 들어:
List<String> array = Arrays.asList("one", "two");
Stream<String> stringStream = array.stream().map(value -> {
	System.out.println("Executed");
	return value.toUpperCase();
});
이것은 Java 8의 Stream API의 예입니다. 이 코드를 실행하면 "Executed"가 표시되지 않는 것을 볼 수 있습니다. 즉, Java에서 스트림이 생성되면 스트림이 즉시 시작되지 않습니다. 대신 누군가가 값을 원할 때까지 기다립니다. 그러나 CompletableFuture누군가가 값을 요청할 때까지 기다리지 않고 즉시 파이프라인 실행을 시작합니다. 나는 이것을 이해하는 것이 중요하다고 생각합니다. 그래서 우리는 CompletableFuture. 파이프라인(또는 체인)을 어떻게 만들 수 있으며 어떤 메커니즘을 가지고 있습니까? 이전에 작성한 기능적 인터페이스를 기억하십시오.
  • A를 취하고 B를 반환하는 a가 있습니다. Function단일 메서드가 있습니다. apply().
  • ConsumerA를 취하고 아무것도 반환하지 않는 a가 있습니다 (Void). 단일 방법이 있습니다: accept().
  • Runnable스레드에서 실행되고 아무 것도 받지 않고 아무 것도 반환하지 않는 가 있습니다 . 단일 방법이 있습니다: run().
다음으로 기억해야 할 것은 작업에서 , 및 를 CompletableFuture사용한다는 것입니다. 따라서 다음을 사용하여 다음을 수행할 수 있음을 항상 알 수 있습니다 . RunnableConsumersFunctionsCompletableFuture
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);
}
thenRun(), thenApply()및 메서드 thenAccept()에는 "비동기" 버전이 있습니다. 이는 이러한 단계가 다른 스레드에서 완료됨을 의미합니다. 이 스레드는 특수 풀에서 가져오므로 새 스레드인지 이전 스레드인지 미리 알 수 없습니다. 그것은 모두 작업이 얼마나 계산 집약적인지에 달려 있습니다. 이러한 방법 외에도 세 가지 더 흥미로운 가능성이 있습니다. 명확성을 위해 어딘가에서 어떤 종류의 메시지를 받는 특정 서비스가 있다고 가정해 보겠습니다. 여기에는 시간이 걸립니다.
public static class NewsService {
	public static String getMessage() {
		try {
			Thread.currentThread().sleep(3000);
			return "Message";
		} catch (InterruptedException e) {
			throw new IllegalStateException(e);
		}
	}
}
이제 제공하는 다른 능력을 살펴보겠습니다 CompletableFuture. CompletableFuturea의 결과를 다른 결과와 결합할 수 있습니다 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();
스레드는 기본적으로 데몬 스레드이므로 명확성을 위해 get()결과를 기다리는 데 사용합니다. 결합할 수 있을 뿐만 아니라 CompletableFutures다음을 반환할 수도 있습니다 CompletableFuture.
CompletableFuture.completedFuture(2L)
				.thenCompose((val) -> CompletableFuture.completedFuture(val + 2))
                               .thenAccept(result -> System.out.println(result));
여기서 나는 이 방법이 간결함을 위해 사용되었다는 점에 주목하고 싶습니다 CompletableFuture.completedFuture(). 이 메서드는 새 스레드를 생성하지 않으므로 나머지 파이프라인은 호출된 동일한 스레드에서 실행됩니다 completedFuture. 방법 도 있습니다 thenAcceptBoth(). 이것은 와 매우 유사 accept()하지만 thenAccept()a를 받아들이 ConsumerthenAcceptBoth()또 다른 CompletableStage+를 BiConsumer입력으로 받아들입니다. 즉 a consumer는 하나가 아닌 2개의 소스를 받습니다. 이름에 "Either"라는 단어가 포함된 메서드가 제공하는 또 다른 흥미로운 기능이 있습니다. 더 나은 조합: Java와 Thread 클래스.  4부 — Callable, Future 및 친구 - 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));
이 코드는 예외가 발생하고 다른 일이 발생하지 않기 때문에 아무 작업도 수행하지 않습니다. 그러나 "예외적으로" 문장의 주석을 제거하여 예상되는 동작을 정의합니다. 에 대해 말하자면 CompletableFuture다음 비디오를 시청하는 것이 좋습니다. 겸손한 생각으로, 이것들은 인터넷에서 가장 설명적인 비디오 중 하나입니다. 그들은 이 모든 것이 어떻게 작동하는지, 우리가 사용할 수 있는 툴킷이 무엇인지, 그리고 이 모든 것이 왜 필요한지 명확히 해야 합니다.

결론

바라건대, 스레드가 완료된 후 계산을 가져오기 위해 스레드를 사용하는 방법이 이제 명확해졌습니다. 추가 자료: 더 나은 조합: Java와 Thread 클래스. 1부 — 실행 스레드 Java와 Thread 클래스를 함께 사용하면 더 좋습니다. 2부 — 동기화 함께 하면 더 좋습니다: Java와 Thread 클래스. 3부 — 상호 작용 더 나은 조합: Java와 Thread 클래스. 파트 V — Executor, ThreadPool, Fork/Join 더 나은 조합: Java 및 Thread 클래스. 6부 — 발사!
코멘트
  • 인기
  • 신규
  • 이전
코멘트를 남기려면 로그인 해야 합니다
이 페이지에는 아직 코멘트가 없습니다