개발 학습 실습실

이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.

공부 › Java › 스레드와 동시성

스레드 풀

스레드를 늘리면 무엇이 달라지나

스레드를 요청마다 새로 만들고 버리는 대신, 한 번 만든 스레드를 정해진 수 안에서 돌려 쓰는 것이 스레드 풀입니다. 요청이 들어오면 스레드 하나가 그 요청을 끝까지 맡습니다. 기본 수만큼의 스레드가 모두 일하고 있으면 요청은 대기 줄에 서고, 줄이 꽉 차면 최대 수까지 스레드를 더 만들며, 그래도 넘치면 거절됩니다. 스레드 수와 대기 줄의 크기에 따라 대기와 거절이 어떻게 달라지는지 봅니다.

실행으로 대조 · ThreadPoolExecutor의 실제 실행 결과와 대조

이 실습에 나오는 개념

스레드 풀
스레드를 필요할 때마다 만들고 없애는 대신 일정 수의 스레드를 유지하며 재사용하는 구조다. 스레드 생성 비용을 줄이고, 동시에 실행되는 스레드 수를 제한해 과도한 메모리 사용과 컨텍스트 스위칭을 막는다. 처리할 작업은 큐에 쌓이고 빈 스레드가 가져가 실행한다. 톰캣의 요청 처리, DB 커넥션 풀도 같은 발상이다.
ThreadPoolExecutor의 동작 순서
자바의 스레드 풀 구현은 작업이 들어오면 다음 순서로 처리한다. 실행 중인 스레드가 corePoolSize보다 적으면 새 스레드를 만든다. 그 이상이면 작업을 큐에 넣는다. 큐가 가득 차면 maximumPoolSize까지 스레드를 더 만든다. 그것도 넘으면 작업을 거부한다. 크기가 정해진 큐일 때는 큐가 가득 찬 뒤에야 스레드가 늘어나며(무제한 큐면 늘지 않는다) 이 점이 직관과 다르다.
톰캣의 스레드 모델
톰캣은 요청 처리용 스레드를 풀로 관리하며, 요청 하나를 스레드 하나가 맡아 응답이 끝날 때까지 처리한다. 동시에 처리할 수 있는 요청 수는 최대 스레드 수(기본 200)로 제한되고, 모든 스레드가 사용 중이면 새 요청은 대기한다. 요청 처리 중 데이터베이스나 외부 호출을 기다리는 동안에도 스레드는 점유된 상태다.
블로킹과 논블로킹
블로킹은 호출한 작업이 끝날 때까지 호출한 스레드가 멈춰서 다른 일을 하지 못하는 것이다. 논블로킹은 작업이 끝나지 않았어도 호출이 바로 반환되어 스레드가 다른 일을 할 수 있는 것이다. 일반적인 JDBC 호출은 DB 응답이 올 때까지 스레드가 멈추는 블로킹 호출이다.

이렇게 바꿔 보세요

처음 설정으로 실행하면

  1. 요청 1 도착. 스레드가 기본 수(2개)보다 적으므로 스레드 1을 새로 만들어 바로 처리합니다.
  2. 요청 2 도착. 스레드가 기본 수(2개)보다 적으므로 스레드 2를 새로 만들어 바로 처리합니다.
  3. 요청 3 도착. 스레드 2개가 모두 일하는 중입니다. 스레드를 늘리지 않고 대기 줄에 세웁니다(대기 1/2).
  4. 요청 4 도착. 스레드 2개가 모두 일하는 중입니다. 스레드를 늘리지 않고 대기 줄에 세웁니다(대기 2/2).
  5. 요청 5 도착. 대기 줄이 꽉 찼습니다. 이제서야 스레드 3을 추가로 만들어 처리합니다(최대 4개까지).
  6. 요청 6 도착. 대기 줄이 꽉 찼습니다. 이제서야 스레드 4를 추가로 만들어 처리합니다(최대 4개까지).
  7. 요청 7 도착. 스레드 4개가 모두 일하는 중이고 대기 줄도 꽉 찼습니다. 요청을 거절합니다.
  8. 요청 8 도착. 스레드 4개가 모두 일하는 중이고 대기 줄도 꽉 찼습니다. 요청을 거절합니다.
  9. 요청 9 도착. 스레드 4개가 모두 일하는 중이고 대기 줄도 꽉 찼습니다. 요청을 거절합니다.
  10. 요청 10 도착. 스레드 4개가 모두 일하는 중이고 대기 줄도 꽉 찼습니다. 요청을 거절합니다.
  11. 스레드 1이 요청 1을 끝내고, 대기 줄 맨 앞의 요청 3을 꺼내 처리하기 시작합니다. 요청 3은 1.6초를 기다렸습니다.
  12. 스레드 2가 요청 2를 끝내고, 대기 줄 맨 앞의 요청 4를 꺼내 처리하기 시작합니다. 요청 4는 1.6초를 기다렸습니다.
  13. 스레드 3이 요청 5를 끝냈습니다. 대기 중인 요청이 없어 쉽니다.
  14. 스레드 4가 요청 6을 끝냈습니다. 대기 중인 요청이 없어 쉽니다.
  15. 스레드 1이 요청 3을 끝냈습니다. 대기 중인 요청이 없어 쉽니다.
  16. 스레드 2가 요청 4를 끝냈습니다. 대기 중인 요청이 없어 쉽니다.

실행 결과

코드

ExecutorService pool = new ThreadPoolExecutor(
    2,                        // 기본 스레드 수
    4,                        // 최대 스레드 수
    60, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(2));     // 대기 큐 2칸

for (int i = 1; i <= 10; i++) {
    pool.execute(() -> handle());   // 요청 하나에 2.0초
    Thread.sleep(200);            // 0.2초마다 도착
}

이 모형의 전제

요청 하나를 처리하는 동안 스레드 하나가 처리 시간만큼 통째로 묶입니다(응답이 느린 DB나 외부 API를 기다리는 요청). 스레드를 늘려도 비용이 들지 않는다고 가정했습니다. 실제로는 스레드마다 메모리를 쓰고, CPU를 쓰려는 스레드가 코어 수보다 많아지면 교대하는 비용이 늘어납니다. 스레드를 배정하는 순서(기본 스레드 → 대기 큐 → 최대 스레드 → 거절)는 자바 ThreadPoolExecutor의 실제 규칙과 같고, 실제 실행 결과와 대조했습니다.

앞 실습: 교착 상태 · 다음 실습: 스레드 수와 컨텍스트 스위칭

이 실습에 나오는 개념

이 실습에서 보는 것

설정

코드


      
    

그림

실행 비교

이렇게 바꿔 보세요