이 포스팅의 코드 및 정보들은 강의를 들으며 정리한 내용을 토대로 작성한 것입니다.
Concurrent Programming을 할 때 Runnable 인터페이스로 스레드를 통한 실습을 했다.
Callable 인터페이스(call())는 Runnable과는 다르게 어떤 결과를 리턴할 수 있다. (Runnable의 run()은 반환 타입이 void이다.)
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
Callable<String> stringCallable = new Callable<String>() {
@Override
public String call() throws Exception {
return null;
}
};
executorService.shutdown();
}
우선 Callable< String >형 함수형 인터페이스를 만들고
// Callable<String> stringCallable = () -> "StringCallable";
Callable<String> stringCallable = () -> {
Thread.sleep(2000L);
return "StringCallable";
};
Future<String> submit = executorService.submit(stringCallable);
바디에는 2초 동안 잠들고 StringCallable
을 리턴하도록 구현했으며, executorService.submit()은 Future<String>
타입이 반환된다.
즉, Callable이 반환하는 타입의 Future를 받을 수 있다.
Future<String> submit = executorService.submit(stringCallable);
// ExecutionException, InterruptedException
submit.get();
이때 주의할 것은 get() 이전까지는 코드가 쭉 실행되다가 get()을 만난 순간 결과 값을 가져올 때까지 기다린다.
이렇게 get()으로 인해 Blocking Call이 발생되는 것이다.
Started는 빨리 치우지만, 2초 정도 기다렸다가 End가 출력되는 걸 볼 수 있다.
get()으로 계속 기다리고 있다면, Callable을 썼다고 애플리케이션이 빨라지지는 않는 것으로 볼 수도 있다.
마냥 상태를 알기 위해 기다릴 수는 없으므로 isDone()을 통해서 상태를 체크할 수 있으며, 끝났으면 True, 아직 안 끝났으면 False를 반환한다.
진행 중인 작업을 취소하는 기능도 제공되는데, 이 기능을 사용하는 메서드가 cancel()이다.
취소 하느냐 마느냐에 따라 boolean 타입으로 파라미터를 받으며, true를 하게 되면 현재 진행 중인 작업을 interrupt하면서 종료하고, false는 기다린다. false로 기다렸다 한들 cancel()을 호출하면 get()을 해서 가져올 수 없다.
cancel(false)
로 실행을 하게 된다면, 밑에 isDone()으로 체크했을 때 true가 나오게 된다. 작업이 종료됐으니 값을 가져가라는 것이 아니라, cancel()
자체가 실행됐기 때문에 종료가 되는 것이다.
cancel()을 했을 때 아무리 interrupt 안 하고 cancel을 한다고 하더라도 값을 가져올 수 없다. 그렇다고 프로세스가 종료되는 것은 아니다.
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newSingleThreadExecutor();
Callable<String> stringCallable = () -> {
Thread.sleep(2000L);
return "StringCallable";
};
Callable<String> javaCallable = () -> {
Thread.sleep(3000L);
return "JavaCallable";
};
Callable<String> eightVersionCallable = () -> {
Thread.sleep(1000L);
return "EightVersion";
};
executorService.shutdown();
}
이렇게 여러 Callable이 있을 때 이를 동시에 실행시키고 싶으면 invokeAll()을 활용하면 된다.
invokeAll()의 파라미터에는 값들을 쭉 받아서 ArrayList타입으로 반환하는 asList()에다가 stringCallable, javaCallable, eightVersionCallable 등 동시에 수행하고자 하는 작업들을 넣는다. 반환 값으로 Future의 List타입이 나온다.
한 번에 쭉 출력되긴 한 것을 볼 수 있다. stringCallable이 2초, javaCallable이 3초, eightVersionCallable이 1초 동안 자다가 실행되면 분명 eightVersionCallable이 먼저 끝날 것 같은데, invokeAll()이 실행되면서 각 작업들이 다 끝날 때까지 기다리게 된다. 그래서 출력도 stringCallable부터 위에서 아래로 구현한 순서로 출력되는 것을 볼 수 있다.
여러 주식 정보 주가를 조사 후 내가 가지고 있는 주식의 현재 시가를 다 가져와서, 현재 보유하고 있는 총 자산이 얼마인가를 계산할 때는 모든 주가의 가격을 다 가져와야 하므로 invokeAll()이 필요할 수 있다.
그러나 만약, 주식이 아니라 똑같은 파일을 복사 후 가져와야 하는 경우에는 복사된 서버의 데이터를 다 기다릴 필요는 없다.
기다릴 필요 없이 제일 빠른 복사본 데이터가 필요할 때, 이에 활용할 수 있는 메서드가 invokeAny()이다.
싱글 스레드에서는 EightVersion이 아닌 먼저 자원을 점유한 StringCallable이 먼저 출력된다.
작업이 동시에 들어가려면 스레드 풀이 3개 정도는 들어갈 수 있어야 한다. 3개의 스레드 풀에서 main()을 실행하면 동시에 들어가서 먼저 작업이 끝날 EightVersion이 먼저 출력되는 것을 확인할 수 있다.
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<String> stringCallable = executorService.submit(() -> {
Thread.sleep(2000L);
return "Callable";
});
System.out.println("Hello");
String result = stringCallable.get();
System.out.println(result);
executorService.shutdown();