자바 Concurrent 프로그래밍
Concurrent 하다의 의미는 동시에 여러 작업을 할 수 있는 것을 의미한다. 예를 들어 유튜브를 보면서 게임을 한다거나 노래를 들으면서 코딩을 하는 것들이 Concurrent 한 것이다.
자바에서 지원하는 Concurrent 프로그래밍에는 다음과 같은 것들이 있다.
- 멀티 프로세싱(ProcessBuilder)
- 멀티 스레드(Thread / Runnable)
멀티 스레드는 Thread를 상속해서 다음과 같이 구현할 수 있다.
public class ConcurrentClass {
public static void main(String[] args) {
HelloThread helloThread = new HelloThread();
helloThread.start();
System.out.println("hello : " + Thread.currentThread().getName());
}
static class HelloThread extends Thread {
@Override
public void run() {
System.out.println("world : " + Thread.currentThread().getName());
}
}
}
Thread 클래스는 Runnable 인터페이스를 구현한 클래스이기 때문에 람다를 이용해 간단하게 표현할 수 있다.
public class ConcurrentClass {
public static void main(String[] args) {
Thread thread = new Thread(() -> System.out.println("world : " + Thread.currentThread().getName()));
thread.start();
System.out.println("hello : " + Thread.currentThread().getName());
}
}
Thread의 주요 기능은 다음과 같다.
- 현재 스레드 멈추기(sleep) : 다른 스레드가 처리할 수 있도록 기회를 주지만 락을 놔주지는 않는다.
- 다른 스레드 깨우기(interrupt) : 다른 스레드를 깨워서 InterruptedException을 발생시킨다.
- 다른 스레드 기다리기(join) : 다른 스레드가 끝날 때까지 기다린다.
Executors
Executors는 고수준 Concurrency 프로그래밍을 할 때 쓰인다. 스레드를 만들고 관리하는 작업을 애플리케이션에서 분리해 Executors에게 위임한다.
Executors가 하는 일과 주요 인터페이스는 다음과 같다.
- 스레드 만들기 : 애플리케이션이 사용할 스레드 풀을 만들어 관리한다.
- 스레드 관리 : 스레드 라이프 사이클을 관리한다.
- 작업 처리 및 실행 : 스레드로 실행할 작업을 제공할 수 있는 API를 제공한다.
- Executor : executor(Runnable)
- ExecutorService : Executor를 상속받은 인터페이스로, Callable도 실행 가능하고, Executor를 종료시키거나 여러 Callable을 동시에 실행하는 등의 기능을 제공한다.
- ScheduledExecutorService : ExecutorService를 상속받은 인터페이스로 특정 시간 이후에 또는 주기적으로 작업을 실행할 수 있다.
다음과 같이 ExecutorService로 작업을 실행하고 멈출 수 있다.
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(() -> {
System.out.println("Hello :" + Thread.currentThread().getName());
});
executorService.shutdown(); // 처리중인 작업 기다렸다가 종료
executorService.shutdownNow(); // 당장 종료
Callable과 Future
Callable은 Runnable과 유사하지만 작업의 결과를 받을 수 있다. Future은 비동기적인 작업의 현재 상태를 조회하거나 결과를 가져올 수 있다.
결과를 받아서 출력하고 싶다면 다음과 같이 get() 메서드로 구현할 수 있다. get() 메서드는 타임아웃을 지정해서 최대한으로 기다릴 시간을 설정할 수 있고, 블록킹 콜이다.
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<String> helloFuture = executorService.submit(() -> {
Thread.sleep(2000L);
return "Callable";
});
String result = helloFuture.get();
System.out.println(result);
executorService.shutdown();
기타 API들을 살펴보자.
작업 상태 확인하기
- isDone() : 작업을 완료했으면 true, 아니면 false를 리턴한다.
작업 취소하기
- cancel() : 작업을 취소했으면 true, 못했으면 false를 리턴한다.
- 파라미터로 true를 전달하면 현재 진행 중인 스레드를 interrupt 하고 그렇지 않으면 현재 진행 중인 작업이 끝날 때까지 기다린다.
여러 작업 동시에 실행하기
- invokeAll() : 동시에 실행한 작업 중에 제일 오래 걸리는 작업만큼 시간이 걸린다.
여러 작업 중에 하날도 먼저 응답이 오면 끝내기
- invokeAny() : 동시에 실행한 작업 중에 제일 짧게 걸리는 작업만큼 시간이 걸린다.
- 블록킹 콜이다.
CompletableFuture
CompletableFuture 인터페이스는 자바에서 비동기(Asynchronous) 프로그래밍을 가능하게 하는 인터페이스이다. Future를 사용해도 어느 정도 가능했지만 다음과 불편한 점들이 있었다.
- Future를 외부에서 완료시킬 수 없다. 취소하거나 get()에 타임아웃을 설정할 수는 있다.
- 블록킹 코드(get() 메서드)를 사용하지 않고는 작업이 끝났을 때 콜백을 실행할 수 없다.
- 여러 Future를 조합할 수 없다.
- 예외 처리용 API를 제공하지 않는다.
주요 API에는 다음과 같은 것들이 있다.
비동기로 작업 실행하기
- runAsync() : 리턴 값이 없는 경우
- supplyAsync() : 리턴 값이 있는 경우
- 원하는 Executor(스레드 풀)를 사용해서 실행할 수 있다. 기본은 ForkJoinPool.commonPool()
콜백 제공하기
- thenApply(Funtion) : 리턴 값을 받아서 다른 값으로 바꾸는 콜백
- thenAccept(Consumer) : 리턴 값을 받아서 또 다른 작업을 처리하는 콜백
- thenRun(Runnable) : 리턴 값을 받지 않고 다른 작업을 처리하는 콜백
- 콜백 자체를 또 다른 스레드에서 실행할 수 있다.
조합하기
- thenCompose() : 두 작업이 서로 이어서 실행하도록 조합
- thenCombine() : 두 작업을 독립적으로 실행하고 둘 다 종료했을 때 콜백 실행
- allOf() : 여러 작업을 모두 실행하고 모든 작업 결과에 콜백 실행
- anyOf() : 여러 작업 중에 가장 빨리 끝난 하나의 결과에 콜백 실행
예외 처리
- exceptionally(Function)
- handle(BiFunction)
Reference
'Java' 카테고리의 다른 글
| [Java] Garbage Collection(1) - 개요 (0) | 2022.04.11 |
|---|---|
| [Java] Java 8의 특징(7) - 기타 변화들(어노테이션, 병렬 정렬, Metaspace) (0) | 2022.02.28 |
| [Java] Java 8의 특징(5) - Date와 Time (0) | 2022.02.26 |
| [Java] Java 8의 특징(4) - Optional (0) | 2022.02.25 |
| [Java] Java 8의 특징(3) - Stream (0) | 2022.02.23 |