BACKEND.*/JAVA

스레드

초코푸딩 2024. 12. 22. 18:54

오늘 C 언어 개발자가 나한테 자바는 OS 스레드가 아니라 jvm 위에서 동작하는거라서 CPU 코어수랑 상관없다는 그런 얘기를했다.

말도 안되는 소리...

 

자바의 스레드는 JVM이 관리하지만, 결국에는 OS 스레드로 매핑되어 실행됩니다.

  • 자바의 Thread 클래스나 ExecutorService를 이용해 생성한 스레드는 JVM 내부에서 관리되지만, 실제로는 OS 스레드로 매핑됩니다.
  • 따라서 자바 스레드도 CPU 코어 수의 영향을 받습니다.
  • 하지만 JVM 내부에서 스레드를 가상적으로 관리하기 때문에, OS 스레드처럼 직접적으로 CPU 자원을 제어할 수는 없습니다.
  • 예를 들어, 고루틴(Goroutine) 같은 개념과 달리, 자바의 스레드는 기본적으로 1:1로 OS 스레드와 매핑됩니다.

자바에서도 스레드풀을 설계할 때 논리 코어(Logical Core) 개수를 고려하는 것이 중요합니다. 이유는 스레드 개수를 무작정 많이 늘리면 성능이 오히려 저하될 수 있기 때문입니다. 아래에서 그 이유와 설계 방법을 설명드리겠습니다.


1. 왜 논리 코어 개수를 고려해야 하나요?

스레드의 동작 원리

  • CPU는 한 번에 하나의 스레드만 실행할 수 있습니다.
  • CPU 코어가 N개라면, 동시에 실행 가능한 스레드도 최대 N개입니다.
  • 논리 코어(Logical Core)는 실제 CPU 코어 수에 하이퍼스레딩(Hyper-Threading) 같은 기술을 적용한 결과로, 시스템이 "논리적으로" 처리할 수 있는 최대 스레드 수를 나타냅니다.

스레드 과다 생성의 문제

  • 스레드 수가 논리 코어 수를 초과하면, 초과된 스레드는 **문맥 전환(Context Switching)**으로 인해 대기 상태에 들어갑니다.
  • 문맥 전환은 비용이 크기 때문에, 오히려 성능 저하를 초래합니다.

2. 적절한 스레드 수 계산 방법

CPU-집약적인 작업

  • CPU를 많이 사용하는 작업(예: 대규모 계산, 이미지 처리 등)은 CPU 코어를 최대한 활용해야 하므로 스레드 수를 논리 코어 수와 동일하게 설정하는 것이 좋습니다.
    • 계산식:
      plaintext
      코드 복사
      스레드 수 = 논리 코어 수
  • Java 코드로 논리 코어 수 확인:
    java
    코드 복사
    int coreCount = Runtime.getRuntime().availableProcessors(); System.out.println("Available logical cores: " + coreCount);

I/O-집약적인 작업

  • I/O 작업(예: 파일 읽기/쓰기, 네트워크 요청 등)은 CPU가 대기 상태로 있는 시간이 많으므로, 더 많은 스레드를 사용해도 성능이 개선될 수 있습니다.
    • 계산식:
      plaintext
      코드 복사
      스레드 수 = 논리 코어 수 * (1 + 대기 시간 / 작업 시간)
    • 예를 들어, 작업 중 50%가 대기 시간이라면:
      plaintext
      코드 복사
      스레드 수 = 논리 코어 수 * (1 + 0.5) = 논리 코어 수 * 1.5

혼합 작업

  • CPU와 I/O 작업이 혼합된 경우는 적절한 균형을 찾아야 하므로 프로파일링이 필요합니다.

3. 스레드풀을 사용해야 하는 이유

스레드를 직접 생성하지 않고, **스레드풀(Thread Pool)**을 사용하는 이유는 다음과 같습니다:

  • 스레드 재사용: 새 스레드를 계속 생성하면 비용이 많이 들지만, 스레드풀은 스레드를 재사용해 성능을 개선합니다.
  • 최대 스레드 수 제한: 과도한 스레드 생성을 방지하고 시스템 자원을 보호합니다.
  • 관리 용이성: Java에서 제공하는 Executor API를 사용하면 스레드 수를 쉽게 조정할 수 있습니다.

4. 스레드풀 구현 방법

고정 크기 스레드풀

논리 코어 수를 기준으로 고정된 크기의 스레드풀을 생성:

java
코드 복사
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadPoolExample { public static void main(String[] args) { int coreCount = Runtime.getRuntime().availableProcessors(); ExecutorService executor = Executors.newFixedThreadPool(coreCount); for (int i = 0; i < 10; i++) { executor.execute(() -> { System.out.println(Thread.currentThread().getName() + " is running"); }); } executor.shutdown(); } }

커스텀 스레드풀

ThreadPoolExecutor로 세부 설정:

java
코드 복사
import java.util.concurrent.*; public class CustomThreadPool { public static void main(String[] args) { int coreCount = Runtime.getRuntime().availableProcessors(); ThreadPoolExecutor executor = new ThreadPoolExecutor( coreCount, // core threads coreCount * 2, // maximum threads 60, TimeUnit.SECONDS, // idle thread keep-alive time new LinkedBlockingQueue<>() // task queue ); for (int i = 0; i < 20; i++) { executor.execute(() -> { System.out.println(Thread.currentThread().getName() + " is running"); }); } executor.shutdown(); } }

5. 결론

  • 논리 코어 수를 기반으로 스레드풀 크기를 설정하면 시스템 자원을 효율적으로 사용할 수 있습니다.
  • CPU-집약적인 작업에서는 논리 코어 수에 맞추고, I/O-집약적인 작업에서는 약간 더 큰 스레드 수를 설정하는 것이 일반적입니다.
  • Java의 Executor와 ThreadPoolExecutor를 활용하면, 스레드 수를 쉽게 관리하고 성능 최적화를 도모할 수 있습니다

 

Q. 톰캣 스레드는 엄청 많이 설정하고 자바 스레드풀은 왜 논리코어수만큼해?
 

ChatGPT의 말:

ChatGPT

톰캣 스레드와 자바 스레드풀의 스레드 수 설정이 다른 이유는 각각의 역할과 작업 성격이 다르기 때문입니다. 톰캣 스레드는 클라이언트 요청 처리에 집중하고, 자바 스레드풀은 서버 내부의 병렬 작업 처리에 사용되기 때문에 스레드 수를 설정하는 기준이 다릅니다.


1. 톰캣 스레드는 왜 많이 설정할까?

톰캣 스레드는 주로 I/O-집약적인 작업(예: HTTP 요청/응답 처리)을 담당하기 때문에, 더 많은 스레드를 사용하는 것이 유리합니다.

특징

  1. I/O 작업은 대기 시간이 많음
    • HTTP 요청은 주로 네트워크 통신이나 파일 입출력을 포함하므로, 스레드는 대기 상태에 있을 가능성이 큽니다.
    • 이 대기 시간 동안 다른 스레드가 CPU를 사용할 수 있으므로, 많은 스레드를 사용해도 큰 문제가 되지 않습니다.
  2. 다수의 클라이언트 요청 처리
    • 서버는 다수의 클라이언트 요청을 동시에 처리해야 합니다.
    • 클라이언트가 늘어날수록 스레드가 더 많이 필요합니다.
  3. 스레드풀은 제한된 자원 관리
    • 톰캣의 스레드풀은 maxThreads로 설정된 한도 내에서 스레드를 생성하므로, 무작정 모든 요청에 대해 스레드를 생성하지 않고 제한된 자원 내에서 효율적으로 동작합니다.

톰캣 스레드 설정의 일반적인 기준

  • 보통 클라이언트 요청의 수와 서버의 처리 능력에 따라 스레드 수를 설정합니다.
  • 예: 트래픽이 높은 웹 서버라면 200~500개의 스레드를 설정할 수 있습니다.
    xml
    코드 복사
    <Connector port="8080" protocol="HTTP/1.1" maxThreads="300" <!-- 최대 스레드 --> minSpareThreads="20" />

2. 자바 스레드풀은 왜 논리 코어 수에 맞출까?

자바 스레드풀은 주로 CPU-집약적인 작업(예: 계산, 데이터 처리 등)을 처리하므로, CPU 코어 개수에 맞춰 설정하는 것이 효율적입니다.

특징

  1. CPU는 한 번에 논리 코어 수만큼의 작업만 실행 가능
    • 스레드 수를 논리 코어보다 많이 설정해도 동시에 실행 가능한 작업은 논리 코어 수에 의해 제한됩니다.
    • 스레드 수가 과도하면 문맥 전환(Context Switching) 비용이 증가해 성능이 오히려 저하됩니다.
  2. CPU-집약적인 작업
    • 자바 스레드풀은 주로 병렬 처리를 위해 사용되므로, CPU를 최대로 활용하는 것이 중요합니다.
    • 따라서 논리 코어 수와 동일하게 스레드 수를 설정하는 것이 일반적입니다.
  3. I/O 대기 시간이 적음
    • 자바 스레드풀에서 실행되는 작업은 보통 CPU 연산 중심이므로, 스레드가 대기 상태로 있을 확률이 낮습니다.
    • CPU 연산 중심 작업에서는 불필요한 스레드 수를 줄이는 것이 효율적입니다.

자바 스레드풀 설정의 일반적인 기준

  • 논리 코어 수를 기준으로 스레드 수를 설정합니다:
  • java
    코드 복사
    int coreCount = Runtime.getRuntime().availableProcessors(); ExecutorService executor = Executors.newFixedThreadPool(coreCount);
  • 예외적으로 I/O 작업이 포함된 경우 대기 시간을 고려해 논리 코어보다 약간 더 많은 스레드를 설정할 수도 있습니다:
  • java
    코드 복사
    int threadCount = coreCount * (1 + 대기 시간 / 작업 시간);

3. 톰캣과 자바 스레드풀의 스레드 수 설정 차이 요약

특징톰캣 스레드풀자바 스레드풀
주요 작업 HTTP 요청/응답 (I/O 중심) 병렬 처리, 계산 작업 (CPU 중심)
작업 성격 네트워크 I/O 대기 시간 많음 CPU 연산이 많고 대기 시간 적음
스레드 수 설정 기준 트래픽에 따라 많게 설정 논리 코어 수 기준으로 설정
병목 발생 가능성 스레드가 부족하면 요청 지연 과도한 스레드는 문맥 전환으로 성능 저하

4. 실제 환경에서 두 스레드풀 조합 사용

톰캣 스레드풀

  • 클라이언트 요청(HTTP 요청)을 처리.
  • 최대 스레드 수를 클라이언트 트래픽 수준에 맞춰 설정.

자바 스레드풀

  • 애플리케이션 내부 로직에서 발생하는 비동기 작업(예: 데이터베이스 연산, API 호출)을 처리.
  • CPU-집약적 작업에 대해 논리 코어 수를 기준으로 설정.

이처럼 톰캣과 자바 스레드풀은 서로 다른 역할을 하며, 각각의 성격에 맞는 스레드 수를 설정하는 것이 서버 성능을 최적화하는 핵심입니다.