[Java 멀티스레드] 멀티스레드 정리

벼랑 끝 코딩·2025년 3월 11일

Java MultiThread

목록 보기
6/6

자바 멀티스레드 시리즈에서 멀티스레드에 대해 열심히 학습해봤다.
굉장히 방대한 내용이고 완벽히 이해하려면 직접 부딪혀보며 배워야할 것 같다.
기초를 다지기 위해 초석부터 시작했지만,
결국 멀티스레드에서는 배운 내용 전부를 다루지는 않는다.

멀티스레드를 결론적으로 어떻게 다룰지에 대해 정리해보자.

Callable

Thread → Runnable → Callable

초기에는 Thread를 직접 생성했지만, 상속 관련해서 문제가 있었다.
그래서 인터페이스인 Runnable을 사용했다.

하지만 Runnable에서도 반환 타입과 예외에서 단점이 있었고
결국에는 Callable을 구현해서 사용하기로 한다.

작업(task)은 Callable을 구현하자.

ThreadPoolExecutor, Future

Thread → ThreadPool(ExecutorService, ThreadPoolExecutor)

스레드를 단독으로 사용하면 자원 소모에서 오는 위험이 있었다.
그렇기 때문에 스레드 풀을 생성해서 안전한 시스템 환경을 구축했다.

그렇다면 스레드를 얼만큼 관리하는 것이 좋을까?
다양한 전략이 있었지만 결국은 스레드 풀의 구성 정보를 직접 설정하면서
스레드 풀 전략을 혼합해서 가져갔다.

ExecutorService executorService = new ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize, int keepAliveTime, TimeUnit unit,
new BlockingQueue(), new ThreadPoolExecutor.POLICYTYPE())

적절한 ThreadPool의 Size를 설정하는 것이 서비스 운영의 핵심이다.
장애 발생 시 하드웨어를 지원하는 클라우드 컴퓨팅 환경이라면 서버를 증설하면 되지만,
그게 아니라면 ThreadPool Size를 적절하게 설정하는 것이 해결 방법이 될 수 있다.

스레드 풀 기능을 사용하기 위해서는 ExecutorService 인터페이스를 사용하고,
그 구현체로 ThreadPoolExecutor가 있었다.
ThreadPoolExecutor에 스레드에 대한 정보를 파라미터로 전달하여 스레드 풀을 생성할 수 있다.
기본 스레드, 최대 스레드, 생존 시간, 거절 정책 등을 시스템 환경을 고려하여 적절히 설정하자.

Future

Future<T> future = executoreService.submit(new Callable())

ThreadPoolExecutor 구현체를 생성하여 ExecutorService 객체를 만들면,
수행해야할 작업인 Callable을 전달하여 멀티스레드 환경을 구축할 수 있었다.
Callable은 예외를 던질 수 있으며, 작업 결과를 반환할 수 있었다.

결과는 Future 객체를 통해 받을 수 있기 때문에
get(), cancel()과 같은 Future 메서드를 호출해서 작업(task)을 제어할 수 있다.

변경점

Thread에서 다양한 메서드를 학습했다.
하지만 스레드를 스레드 풀로 관리하기 시작하면서 Thread를 직접적으로 드러내지 않게 됐다.
따라서 데몬 스레드에 대한 개념도 없어졌고, Thread 메서드도 사용할 일이 없어졌다!

join() 메서드 대신에 get()이라는 Blocking 메서드를 호출할 수 있었고,
yield() 메서드를 직접 호출할 일 없이 스레드 풀에 자체적으로 스케줄링 기능이 추가됐다.
interrupt() 메서드 대신에 작업을 제어할 수 있는 Future 메서드를 활용 가능했다.

스레드의 상태(RUUNABLE/BLOCKED/WAITING/TIMED_WAITING/TERMINATED)와 같은 개념은 당연히 이해해야 하지만,
앞으로 우리는 작업 제어 관점에서
Thread 객체에 초점을 맞추기보다 Future 객체에 초점을 맞추면 되겠다.

ReentrantLock

synchronized → Object → ReentrantLock

synchronized 메서드를 통해 멀티스레드 상황에서 안전한 환경을 구축하려 했지만,
무한 대기와 공정성 문제가 발생하여
BLOCKED 상태를 WAITING 상태로 관리하기 위한 Object의 기능이 등장했다.
하지만 Object의 기능은 효율적으로 스레드를 다룰 수 없었고,

결국 최종적으로 ReentrantLock를 사용해야 한다는 결론에 도달했다.

ReentrantLock

결론적으로 멀티스레드 환경을 안정적으로 구축하기 위해 ReentrantLock을 사용하면 된다!
ReentrantLock은 BLOCKED 상태의 스레드를 WAITING 상태로 관리하기 위해
내부적으로 LockSupprot를 사용하면서,
Condition 객체를 사용하여 스레드도 효율적으로 관리할 수 있다.

원자적 연산(CAS), Atomic

하지만 락 기능은 안정적으로 운영할 수 있는 대신 그만큼 무거운 기능이다.
충돌이 적고 짧은 연산에서는 ReentrantLock 대신
원자적 연산(CompareAndSet)을 고려해야 한다.

자바는 데이터 타입의 원자적 연산을 위해 Atomic 클래스를 지원한다.

동시성 컬렉션

멀티스레드 상황에서 대부분의 자바 컬렉션 기능은 원자적 연산이 아니다.
따라서 멀티스레드 상황에는 일반 컬렉션이 아닌 동시성 컬렉션을 사용해야 한다.

Collections.synchronizedXXX()

Proxy 기법을 사용하여 컬렉션에 synchronized 키워드를 추가한다.
Collections.synchronizedXXX() 메서드는 컬렉션에 락 기능을 추가하는 메서드이다.

원자적 연산

List → CopyOnWriteArrayList
Set → CopyOnWriteArraySet, ConcurrentSkipListSet
Map → ConcurrentHashMap, ConcurrentSkipListMap
Queue → ConcurrentLinkedQueue
Deque → ConcurrentLinkedDeque
BlockingQueue → ArrayBlockingQueue, LinkedBlockingQueue, PriorityBlockingQueue,
  SynchronousQueue, DelayQueue

위 컬렉션은 락 기능을 기반이 아닌, 원자적 연산 기반으로 동작한다.

마무리

멀티스레드를 이해하기 위해 많은 개념을 학습했다.

작업은 Callable과 Future 객체,
스레드는 스레드 풀에서 관리한다는 점과 ExecutorService와 그 구현체 ThreadPoolExecutor,
락 기능의 ReentrantLock과 원자적 연산
그리고 멀티스레드 환경에서 사용해야할 동시성 컬렉션까지.
멀티스레드에 필요한 개념을 잊지 말자.

profile
복습에 대한 비판과 지적을 부탁드립니다

0개의 댓글