자바와 쓰레드풀, 쓰레드의 생성비용

dropKick·2020년 11월 24일
6

궁금증

어떤 커뮤니티 보다가 JVM에서 쓰레드 질문이 나옴
다들 쓰레드 생성 비용이 크다고 하고 그래서 쓰레드 풀을 쓴다고 하니 알겠는데
대체 자바에서 쓰레드 생성 비용이 얼마나 되길래 그런걸까?
쓰레드 생성 비용이 비싸다면 언제부터 쓰레드 풀이 효율적인걸까?

요약

  • 쓰레드는 단순한 실행 흐름이 아님, 메모리를 소유하고 있음
  • 메모리 할당은 상당히 비싼 작업임
  • 자바는 Executor 인터페이스를 통해 기본적으로 쓰레드 라이프사이클을 관리 가능함
  • 64비트 JVM은 기본적으로 쓰레드 스택 메모리를 1MB 예약 할당함
  • 현대 메모리 할당은 물리메모리의 가상 매핑이기때문에, 최대 사용 시 1MB까지
    (스택 깊이에 따라 달라짐) 기본적으로 16KB의 '물리적 메모리'를 사용
  • 이러한 쓰레드의 무분별한 생성을 막기위해 쓰레드 풀을 사용함
  • 쓰레드 풀은 교착 상태와 무한 대기가 발생할 위험이 있음
  • 쓰레드 풀이 어느정도부터 효율적일까는 어떤 쓰레드 풀을 어떻게 사용하느냐에 따라 달라짐

집고 넘어가야하는 쓰레드

사실 쓰레드도 별도의 영역을 가지고 있다.


나는 쓰레드를 단순히 하나의 실행 흐름이라고만 하는게 이상하다고 생각했는데, 쓰레드는 실행흐름을 포함하는거지 쓰레드 자체도 레지스터와 스택을 가진다.
그래서 앞선 컨텍스트 스위칭글 처럼 쓰레드도 컨텍스트 스위칭을 가지고, 이로 인해서 쓰레드의 생성도 메모리의 할당이라는 비용때문에 수 많은 쓰레드가 생성되길 원하지 않는다.

기본적인 자바의 스레드 생성 비용

  • 쓰레드는 프로세스가 할당받은 메모리를 사용한다
    즉, JVM이 할당받은 메모리 내에서 메모리를 재할당(메모리 커밋)하기 때문에 쓰레드의 생성비용은 고스란히 JVM 메모리의 소비로 이어진다
  • 64비트 Java8과 Java11에선 쓰레드에겐 기본적으로 1MB의 메모리를 예약할당해준다.
    스택의 깊이가 최대로 늘어났을 때 1MB까지 할당되는 것이지만, 그래도 최소한 16KB 이상의 메모리를 소비한다는 점에는 변함이 없다.

이런 쓰레드 비용에서 작업 요청이 갑자기 1천개가 들어온다면..?
쓰레드 메모리 비용 코드

쓰레드 풀

쓰레드 풀(Thread Pool)은 말 그대로 쓰레드의 모음이다.
제한된 리소스를 이용하여 최대한의 효율을 내기 위한 최적화 기법이랄까
특히, Bottle Neck 현상이 발생하는 I/O 작업과 데이터베이스 작업이 주로 해당된다.

  • 상황을 한번 보자
    1. 천 개의 요청이 들어왔다
    2. 천 개의 스레드가 생성되었다
    3. 천 개의 스레드가 작업을 하려한다
    4. 어???? 누가 먼저 접근하지?

결국 천 개의 쓰레드가 아무리 빠르게 생성되더라도 시스템 스케쥴러에 의해 쓰레드의 우선순위를 매번 할당해야한다.

  • 이런 상황에서 쓰레드 풀을 이용하면
    1. 천 개의 요청이 들어왔다
    2. 일정 쓰레드가 이미 생성되어 쓰레드 풀에 의해 라이프 사이클이 관리 된다(우선순위 포함)
    3. 쓰레드 풀에 의해 작업이 큐를 이용하여 우선순위가 배분되고 처리된다

이것만 보면 쓰레드 풀을 사용하는게 정말정말 효율적인 작업인 것 같다

문제점


이렇게 효율적인 쓰레드 풀을 사용했는데 쓰레드 1,2,3에 나누어 배분된 작업 1, 2, 3 중 작업 1이 가장 먼저 끝났을 경우에는 어떻게 될까?

  • 어떻게 되긴 그냥 논다
    '보통'의 쓰레드 풀의 목적은 쓰레드에게 작업을 배분하는 것에 있기때문

리소스를 효율적으로 사용하려고 최적화 했는데 리소스가 놀다니
이런 일을 눈뜨고 지켜볼 수가 없다

쓰레드 풀의 개선, Fork Join Thread Pool

기존 쓰레드 풀을 개선하기 위한 방법으로 Java 7 이상의 쓰레드 풀에서 사용되고있다.

동작은 다음과 같다
1. 작업을 하나의 큰 작업들로써 제공해준다
2. 첫 쓰레드가 작업을 가져와 자신의 로컬 큐에 할당, 분할한다.
3. 두번째 쓰레드가 가져올 작업이 없다면, 첫 쓰레드의 큐에 있는 분할된 작업을 훔쳐간다
4. 나머지 쓰레드도 반복

이렇게 되면 100개의 작업일 때 3개의 쓰레드가 있다면 대략 50, 25, 25 정도의 작업이 수행된다

결론

  • 멀티 쓰레드의 생성비용이 높기 때문에 제한된 리소스를 최대한 효율적으로 사용하고 싶어했다
  • 이를 위해 제한된 쓰레드만 생성시켜 놓는 쓰레드 풀을 통해 개선하려 했다
  • 하지만 노는 쓰레드 문제가 발생했고, 작업을 최대한 균등하게 분배하기 위해 Fork Join Pool 방식을 사용하고 있다
  • 항상 효율적인 쓰레드 풀이란 없다
    상황에 따라 최선의 방법을 사용해야한다

암달의 법칙은 이렇다. “멀티코어를 사용하는 프로그램의 속도는 프로그램 내부에 존재하는 순차적sequential 부분이 사용하는 시간에 의해서 제한된다.” Thread나 Task를 만들어서 ExecutorService에게 제출하는 식으로 동시성 코드를 작성하면 여러 개의 스레드가 동시에 작업을 수행한다. 하지만 프로그램 안에는 Thread나 Task가 포함하지 않는 코드가 존재한다. 여러 개의 스레드가 동시에 작업을 수행하더라도 synchronized 블록이나 데이터베이스, 네트워크 API 호출 등을 만날 때 다른 스레드와 나란히 줄을 서서 순차적으로 작업을 수행 해야 하는 경우도 있다. 암달의 법칙은 프로그램이 낼 수 있는 속도의 상한이 이런 순차적 코드가 사용하는 시간에 의해서 제한된다고 말하는 것이다. 이러한 순차적 코드의 또 다른 이름은 블로킹blocking 콜이다. 문제는 스레드 자체 가 아니라 스레드를 사용하면서 자기도 모르게 만들어내는 블로킹 콜이다. 조금 과장해서 말하자면 자바 개발자가 스레드를 이용해서 만들어내는 ‘동시성’ 코드는 일종의 신기루다. 사실은 코드 곳곳에 존재하는 블로킹 콜, 순차적 코드 때문에 전 체적인 프로그램의 처리율은 이미 상한이 정해져 있지만 여러 개의 스레드가 ‘동시에’ 동작한다는 사실로부터 위안을 받을 뿐이다.
출처: https://hamait.tistory.com/612 [HAMA 블로그]

참고

https://dzone.com/articles/how-much-memory-does-a-java-thread-take
https://3dmpengines.tistory.com/2003
https://hamait.tistory.com/612
https://qastack.kr/programming/5483047/why-is-creating-a-thread-said-to-be-expensive
https://stackoverflow.com/questions/11700763/thread-pool-vs-many-individual-threads

0개의 댓글