Search

자바에서 멀티 쓰레드 사용하기

태그
Java
면접질문
작성 상태
작성 완료
작성일
2022/11/14
참고 링크
참고 링크 2
오늘 인턴 면접에서 질문으로 Callable 과 Thread 사이의 차이점을 말해보라고 질문을 받았다. Callable 은 사실 생전 처음 들어보는거라 당연히 전혀 대답을 하지 못했다. 떨어지면 이거때문이겠지.. 여튼 새로운 공부거리를 던져주었으니, 공부해야겠다는 생각에 이렇게 면접 끝나자마자 찾아보고 있다.

Java 1.0 : Thread 와 Runnable

Thread 클래스 사용법

자바에서 새 쓰레드를 생성하려면 Thread 클래스를 사용한다. 이 클래스로 새 쓰레드를 생성하는 방법에는 크게 2가지가 있다.
1.
Runnable 을 사용하는 방법
Thread thread = new Thread(new Runnable(){ @Override public void run(){ // 쓰레드에서 하고 싶은 작업 } }); thread.start(); //Runnable 인터페이스는 함수형 인터페이스 이므로 람다식으로 쓸 수 있다. Thread lamdaThread = new Thread(() -> {}); lamdaThread.start();
Java
복사
2.
Thread 의 하위 클래스를 생성하는 방법
class MyThread extends Thread{ @Override public void run(){ // 하고싶은 작업 } } Thread thread = new MyThread(); thread.start();
Java
복사
이 두가지 방법중 첫번째 방법이 더 선호된다. 더 확장성이 좋기 때문이다.
Runnable 을 사용하는 방법이 확장성이 더 좋은 이유

Thread 클래스 내부 동작 원리

Thread 클래스의 인스턴스를 생성하고, start() 메서드를 실행시키면 내부적으로 다음 순서대로 메서드가 실행되고 실제 쓰레드가 생성된다.
1.
start() 메서드 실행
2.
start0() 네이티브 메서드 실행 > 이 시점에 실제 쓰레드가 생성된다. 내부에서 run() 메서드를 실행시킨다.
3.
run() 메서드 실행
java 11 기준 Thread 클래스의 코드 일부분

Thread 와 Runnable 사이의 이상한 관계

Thread 클래스는 Runnable 인터페이스를 구현하고 있다
이 부분은 객체지향적으로 바라보았을때 이해할 수 없는 부분이다.
Thread 클래스는 Runnable 객체를 생성자를 통한 의존 주입의 방식으로 외부에서 주입받고, 이를 사용하는 구조이다. 상속을 이용하는 경우에도 굳이 Runnable 을 상속할 이유가 없다. 추측하건데, 최초의 잘못된 설계가 하위 호환성을 위해 지금까지 내려 오는 것 같다.

Thread 와 Runnable 의 문제점

1.
Thread 로 쓰레드를 무진장 많이 생성할 수 있다. ⇒ 쓰레드 풀의 도입 필요
쓰레드의 숫자가 일정 개수 이상으로 늘어나면 성능에 심각한 문제가 생긴다.
2.
쓰레드의 실행 결과를 반환 받을 수 없다.
3.
API 가 지나치게 저수준이다.

Java 1.5 : Executors, Callable …

Java 1.5 에서 기존의 멀티 쓰레드를 위한 API 인 ThreadRunnable 의 한계를 극복하기 위해 여러 새로운 API 를 추가하였다.

Callable 과 Future

기존의 Runnable 을 이용해 멀티 스레드에서 수행할 작업을 구현하는 경우, 그 작업의 수행 결과를 반환받을 수 없었다. 이를 위해선 전혀 객체지향적이지 못한 방법을 이용해야만 했다. 이를 해결하기 위해 Callable 인터페이스가 추가되었다. 제네릭을 이용해 작업의 결과를 원하는 타입으로 반환할 수 있다.
@FunctionalInterface public interface Callable<V> { V call() throws Exception; }
Java
복사
Runnable 과 마찬가지로, 단순히 위 인터페이스의 구현체의 메서드를 실행시킨다고 멀티쓰레드에서 작업이 실행되는 것이 아니다. Callable 의 구현체를 멀티쓰레드에서 실행시키기 위해서는 ExecutorService 를 이용해야 한다. 비동기 방식으로 구동하므로, 원래 쓰레드도 계속 작업을 수행할 수 있다.
void future() { ExecutorService executorService = Executors.newSingleThreadExecutor(); Callable<String> callable = new Callable<String>() { @Override public String call() throws InterruptedException { Thread.sleep(3000L); return "Thread: " + Thread.currentThread().getName(); } }; Future<String> future = executorService.submit(callable); //다른 코드를 작성했다면 callable 로 생성한 작업이 끝나지 않았더라도 코드가 실행된다. String str = future.get(); // 이 시점에서 callable 의 작업이 끝날 때 까지 블로킹한다. }
Java
복사
Future 인터페이스는 비동기 작업을 처리하기 위한 인터페이스로, 비동기, 블로킹 방식으로 동작한다. 즉, Future.get() 메서드가 호출되기 전에 메인 쓰레드에선 멀티 쓰레드의 작업 완료 여부와 상관 없이 다른 작업들을 수행할 수 있지만, Future.get() 메서드가 호출되는 순간 멀티 쓰레드의 작업이 끝날 때 까지 메인 쓰레드는 블로킹된다. 물론, 무한정 기다리지 않도록 타임아웃을 설정 할 수 있다.
public interface Future<V> { boolean cancel(boolean mayInterruptIfRunning); boolean isCancelled(); boolean isDone(); V get() throws InterruptedException, ExecutionException; V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException; }
Java
복사

Executors, Executor, ExecutorService : 쓰레드풀의 도입

Java 1.5에서 쓰레드 풀이 도입되었다. 관련된 인터페이스들의 계층도는 다음과 같다.

Executor : 등록된 작업을 실행하는 책임을 맡은 인터페이스

인터페이스 분리 원칙에 따라 등록된 작업을 실행하는 책임 만을 가지는 인터페이스이다. 따라서 전달받은 작업을 실행하는 메서드만 가지고 있다.
public interface Executor { void execute(Runnable command); }
Java
복사

ExecutorService : 비동기 작업의 등록과 종료, 실행을 담당하는 인터페이스

ExecutorService은 비동기 작업의 등록과 종료, 실행을 담당하는 인터페이스이다. 즉, 자바에서 쓰레드 풀을 만드려면 이 인터페이스를 적절히 구현하면 된다.
이 인터페이스에서 제공하는 메서드들을 분류하면
1.
비동기 작업의 등록과 실행을 위한 메서드들
2.
ExecutorService 자신의 라이프 사이클 관리를 위한 메서드들. 즉, 쓰레드 풀 자체의 라이프 사이클을 관리하기 위한 메서드들이다.

ScheduledExecutorService : 주기적인 작업 실행을 위한 인터페이스

ExecutorService 를 상속받은 인터페이스로 특정 작업을 주기적으로 실행시킬 필요가 있을 때 사용한다.

Executors : 팩토리 패턴을 이용해 쓰레드 풀을 생성게 해주는 클래스

위에서 말한 인터페이스들을 이용해 쓰레드 풀을 생성할 수 있는데, 이를 프로그래머가 직접 구현한다면 아주 힘든 고역일 것이다. 이를 위해 자바에서 미리 만들어둔 구현체를 생성할 수 있도록 하는 팩토리 클래스이다.