Thread in Java - 두번째 이야기

lango·2022년 6월 22일
0

자바(Java)

목록 보기
1/4
post-thumbnail

thread-2

Thread와 관련되어 이전에 못다한 이야기를 마저 적어보려고 한다.
첫번째 이야기를 적은지 얼마 지나지 않았는데, 까먹은 부분이 많아 다시 복습을 하고 작성한다.

기본적으로 Thread는 Thread 클래스를 상속하여 구현하거나 Runnable 인터페이스를 구현하는 방식으로 구현한다. 그런데 몇개의 Thread를 사용할 건지, 얼마나의 시간마다 반복할 것인지 등 다수의 작업들을 비동기로 수행한다는 것은 간단하지 않다.

그래서 자바는 ThreadPool을 관리할 수 있도록 java.util.concurrent Package에서 ExecutorService 인터페이스와 Executors 클래스를 제공한다.

Executors

Executors가 생성해주는 ExecutorService는 재사용이 가능한 ThreadPool로 Executors 인터페이스를 확장하여 Thread의 라이프사이클을 제어하며 발생할 수 있는 고려사항들을 개발자가 신경쓰지 않도록 편리하게 추상화한 것이다.

이러한 ThreadPool의 종류를 알아보자.

  • newFixedThreadPool(int)
    인자 갯수만큼 고정된 ThreadPool을 생성한다.
  • newCachedThreadPool()
    호출 당시의 필요한 만큼 ThreadPool의을 생성한다.
    이미 생성된 스레드를 재사용할 수 있기 때문에 성능상의 이점이 있을 수 있다.
  • newScheduledThreadPool(int)
    일정 시간 뒤에 실행되는 작업이나, 주기적으로 수행되는 쓰레드풀을 인자 갯수만큼 생성한다.
  • newSingleThreadExecutor()
    단일 쓰레드인 풀을 생성한다. 단일 쓰레드에서 동작해야 하는 작업을 처리할 때 사용한다.
  • newWorkStealingPool(int parallelism)
    JDK 1.8부터 지원하는 방식으로 시스템에 가용 가능한 만큼 쓰레드를 활용하는 ExecutorService를 생성한다.

다음으로 ThreadPool 작업을 생성하는 메서드를 알아보자.

  • execute()
    작업 처리 결과(void)를 반환하지 않는다.
    작업 처리 도중 예외가 발생하면 스레드가 종료되고 해당 스레드는 스레드 풀에서 제거된다.
    다른 작업을 처리하기 위해 새로운 스레드를 생성한다.

  • submit()
    작업 처리 결과(Future)를 반환한다.
    결과를 받아야 하기에 Callable을 구현한 Task를 인자로 전달한다.
    작업 처리 도중 예외가 발생하더라도 스레드는 종료되지 않고 다음 작업을 위해 재사용한다.
    스레드의 생성 오버헤드를 방지하기 위해서라도 submit() 을 가급적 사용한다.

  • invokeAny()
    실행에 성공한 작업중 하나의 리턴값을 반환한다.
    Task를 Collection에 넣어서 인자로 넘겨줄 수 있다.

  • invokeAll()
    모든 작업의 리턴값을 List<Future<>> 타입으로 반환한다.
    Task를 Collection에 넣어서 인자로 넘겨줄 수 있다.


추가로 작업이 수행된 후 ExecutorService는 자동으로 종료되지 않고 다시 생성될 작업을 처리하기 위해 대기하기 때문에 별도로 종료를 해줘야 한다.

ExecutorService에서는 3가지의 종료 메서드를 제공한다.

  • shutdown()
    작업 큐에 남은 작업을 모두 마무리하고 종료한다.
    오버헤드를 방지하기 위해서 일반적으로 많이 사용한다.

  • shutdownNow()
    작업 큐에 상관없이 강제로 종료한다.

  • awaitTermination()
    이미 수행 중인 작업이 지정된 시간동안 끝나기를 기다리며, 해당 시간 내에 끝나지 않으면 작업 스레드들을 interrupt()시키고 false를 반환한다.




백문이 불여일견이니 실제로 구현해보자.

public void ThreadPoolTest1() {
	ExecutorService executorService = Executors.newFixedThreadPool(10);
	Runnable task = new Runnable() {
    	public void run() {
        	System.out.println("Thread: " + Thread.currentThread().getName());
        }
    };
    for(int i=0; i<10; i++){
    	executorService.execute(task);
    }
}
Thread: pool-1-thread-10
Thread: pool-1-thread-8
Thread: pool-1-thread-2
Thread: pool-1-thread-9
Thread: pool-1-thread-7
Thread: pool-1-thread-6
Thread: pool-1-thread-5
Thread: pool-1-thread-4
Thread: pool-1-thread-3
Thread: pool-1-thread-1

간단하게 newFixedThreadPool(10) 메서드를 이용하여 10개를 고정으로 가지는 Thread Pool을 생성하여 작업을 실행해보았다.

다음으로는 작업에 대한 처리결과를 받아보도록 구현해보자.

public void ThreadPoolTest2() {
	ExecutorService executorService = Executors.newFixedThreadPool(10);
    Callable<String> task = new Callable<String>() {
  		@Override
  		public String call() throws Exception {
  			return Thread.currentThread().getName() + "-job";
        }
    };
  	for(int i=0; i<10; i++){
		Future<String> future = executorService.submit(task);
        try {
        	System.out.println("future: " + future.get());
        } catch (InterruptedException e) {
        	throw new RuntimeException(e);
        } catch (ExecutionException e) {
        	throw new RuntimeException(e);
        }
	}
    executorService.shutdown();
    System.out.println("ThreadPool end");
}
future: pool-1-thread-1-job
future: pool-1-thread-2-job
future: pool-1-thread-3-job
future: pool-1-thread-4-job
future: pool-1-thread-5-job
future: pool-1-thread-6-job
future: pool-1-thread-7-job
future: pool-1-thread-8-job
future: pool-1-thread-9-job
future: pool-1-thread-10-job
ThreadPool end

작업에 대한 처리결과를 얻어야 된다면 작업 객체를 Callable로 생성하면 된다.
반대로 작업에 대한 처리결과가 필요없다면 Runnable을 사용하면 된다.

Callable vs Runnable

  • Runnable: 어떤 객체도 리턴하지 않으며 Exception을 발생시키지 않는다.
  • Callable: 특정 타입의 객체를 리턴하며. Exception을 발생킬 수 있다.

submit() 메소드는 파라미터로 준 Runnable 또는 Callable 작업을 Thread Pool의 작업 큐에 저장하고 즉시 Future 객체를 리턴한다.

Future?

Future 객체는 작업 결과가 아니라 작업이 완료될 때까지 기다렸다가 최종 결과를 얻는데 사용한다. 그래서 Future는 지연 완료(pending Completion) 객체라고 한다.

블로킹 방식의 작업 처리결과

Future의 get() 메소드를 호출하면 스레드가 작업을 완료할 때까지 블로킹되었다가 작업을 완료하면 처리 결과를 리턴한다.


다시 위의 코드에서 출력을 보면 1~10까지 순서대로 future를 찍은 걸 확인할 수 있다.

future.get()은 Future 객체에 어떤 값이 설정될 때까지 기다린다.
submit()에 전달된 Callable이 어떤 값을 리턴하면 그 값을 Future에 설정하기에 순서대로 출력을 받을 수 있는 것이다.

Final..

Thread에 관해 적은 두번째 이야기를 마친다. 굉장히 깊은 내용은 아니지만..
실무에서 사용해봤다고 하지만 기술적 개념에 대해서 다시 복습하고 몰랐던 부분을 다시 공부할 수 있었던 시간이었다.






참조 문서

https://codechacha.com/ko/java-executors/
https://passiflore.tistory.com/35
https://codechacha.com/ko/java-executors/
https://palpit.tistory.com/entry/Java-%EB%A9%80%ED%8B%B0-%EC%8A%A4%EB%A0%88%EB%93%9C-%EC%8A%A4%EB%A0%88%EB%93%9C%ED%92%80ThreadPool
https://gompangs.tistory.com/entry/JAVA-ExecutorService-%EA%B4%80%EB%A0%A8-%EA%B3%B5%EB%B6%80

profile
찍어 먹기보단 부어 먹기를 좋아하는 개발자

0개의 댓글