5회차 - (2025.08.11 / 스레드)

1. 프로세스/스레드 컨텍스트 스위칭

  • 가상 메모리(MMU) 를 곁들여서

  • 컨텍스트 스위칭은 CPU 가 병렬처리, 동시처리를 위해서 꼭 필요한 작업이다.

  • "컨텍스트 스위칭은 안할수록 좋은거에요" 라는 결론은 앞뒤가 안맞는 결론이다.

  • 각 컨텍스트 스위칭에서 어떤 일이 일어나는지만 알면 충분하다.

1.1 프로세스 컨텍스트 스위칭과 가상메모리(MMU)

프로세스 컨텍스트 스위칭은 가상 메모리(Virtual Memory) 를 관리하는 MMU(Memory Management Unit) 에 큰 부담을 준다.

  • 독립적인 가상 주소 공간 : 각 프로세스는 가상 주소 공간을 가진다. CPU 는 이 가상 주소 공간의 주소를 사용하여 명령어를 실행한다. (가상 주소 공간(페이지) 를 통해 실제 물리 주소를 사용한다)

  • MMU 의 역할 : MMU 는 CPU 가 사용하는 가상 주소를 물리 주소(RAM) 의 실제 주소로 변환하는 역할을 한다. 이 변환을 위해서 페이지 테이블(Page Table) 을 사용한다.

  • 컨텍스트 스위칭 오버헤드

    • 프로세스 A 에서 프로세스 B로 컨텍스트 스위칭이 일어나면, 운영체제는 먼저 프로세스 A 의 모든 상태(컨텍스트) 를 PCB(Process Control Block) 에 저장한다.

    • 이어서 운영체제는 프로세스 B의 상태를 PCB 에서 불러와 CPU 레지스터에 로드한다.

    • 가장 중요한 부분은 이 과정에서 MMU 가 참조하는 페이지 테이블 레지스터의 포인터값을 변경하는데, 이로 인해 MMU 내부에 있는 TLB(Translation Lookasize Buffer) 캐시 내용이 무효화 되는 것이 핵심적인 오버헤드로 작용한다.

    • TLB 가 비워지면 새로운 프로세스는 가상 주소를 물리 주소로 변환하게 위해 느린 메인 메모리에 있는 페이지 테이블에 매번 접근해야 하므로 시스템 성능 저하가 발생한다.

1.2 스레드 컨텍스트 스위칭과 가상메모리(MMU)

스레드 컨텍스트 스위칭은 프로세스 컨텍스트 스위칭보다 훨씬 가벼우며, MMU 에 거의 영향을 주지 않는다.

  • 공유하는 가상 공간 : 같은 프로세스에 속한 모든 스레드는 동일한 가상 주소 공간을 공유한다. 즉 모든 스레드가 같은 페이지 테이블을 사용한다.

  • 컨텍스트 스위칭 오버헤드

    • 스레드 A 에서 스레드 B 로 컨텍스트 스위칭이 일어나면, 운영체제는 스레드 A 의 일부 상태(CPU 레지스터, 스택 포인터 등..) 만 TCB(Thread Control Block) 에 저장한다.

    • 이후 스레드 B 의 상태를 TCB 에서 불러와 로드한다.

    • 이때, MMU 가 참조하는 페이지 테이블 레지스터의 포인터값은 변경되지 않는다. 스레드 A 와 B 가 동일한 가상 주소 공간을 사용하므로, MMU 는 기존 페이지 테이블 레지스터 포인터값, TLB 를 유지하면서 사용하면 된다.

    • 이 때문에 스레드 컨텍스트 스위칭은 MMU 관련 오버헤드가 발생하지 않아 매우 빠르다.

2. 자바의 스레드 객체와 운영체제 스레드는 어떤 관계가 있는건가요?

  • 네이티브 메소드 얘기

  • 1:1, M:N

  • 네이티브 스레드 vs 유저 스레드

2.1 모든 것의 시작 : 네이티브 메서드(Native Method)

자바는 OS 에 독립적으로 동작하도록 설계되었지만, 스레드를 생성하고 실행하는 것과 같이 하드웨어와 OS 의 도움이 꼭 필요한 작업들이 있다. 이때 자바는 OS 의 기능을 '빌려' 써야 하는데, 그 다리 역할을 하는 것이 바로 네이티브 메서드이다.

  • 네이티브 메서드란? : 자바 코드 내에서 native 키워드로 선언만 되어 있고, 실제 구현은 C, C++ 같은 언어로 작성된 메서드를 말한다. JVM 은 이 네이티브 메서드를 통해 OS 의 기능을 직접 호출할 수 있다.

  • 스레드 생성 예시 : 우리가 자바에서 Thread 객체를 만들어 thread.start() 를 호출하면, 내부적으로 다음과 같은 일이 발생한다.

    • thread.start() 메서드는 내부적으로 private native void start0() 라는 네이티브 메서드를 호출한다.

    • start0() 메서드는 C, C++ 로 구현되어 있으며, JVM 을 통해 OS 맞는 시스템 콜을 실행한다.

      • 리눅스라면 pthread_create() 함수를 호출한다.

      • 윈도우라면 CreateThread() 함수를 호출한다.

    • OS 는 요청을 받아 실제 스레드(네이티브 스레드) 를 생성하고, JVM 은 이 네이티브 스레드와 우리가 만든 자바 Thread 객체를 연결(매핑) 한다.

  • 즉, 자바 코드는 OS 에 "스레드 하나 만들어줘!" 라고 직접 말하는게 아니라, 네이티브 메서드라는 통역사를 통해 부탁하는 구조이다.

2.2 Level 별 스레드 정리

하드웨어 스레드

  • 코어의 연산 속도는 메모리보다 빠르기 때문에, 메모리에서 데이터를 보내는 시간을 기다리기 아쉽다.

  • 메모리의 데이터를 기다리는 동안 다른 작업을 수행하면 어떻게 될까?

  • 따라서 코어는 연산 작업을 수행하고 메모리의 데이터를 기다리는 동안 다른 스레드의 작업을 수행할 수 있다. (즉, 서로 다른 쓰레드를 실행시킨다. 각각을 하드웨어 스레드라고 한다.)

  • 인텔의 하이퍼 스레딩(hyper-threading) : 물리적인 코어의 갯수보다 많은 논리적 단위의 스레드를 구성한다.

  • 만약 싱글 코어 CPU 에 하드웨어 스레드가 두 개라면 OS 는 이 CPU 를 듀얼 코어로 인식하고 듀얼 코어에 맞춰서 OS 레벨의 스레드들을 스케줄링 한다.

커널 레벨 스레드 (네이티브 스레드)

  • OS 커널 레벨에서 생성되고 관리되는 스레드

  • 커널 스레드의 컨텍스트 스위칭은 커널이 개입 -> 비용 발생

유저 레벨 스레드

  • 스레드 개념을 프로그래밍 레벨에서 추상화 한 것

  • 자바에서 스레드 객체를 생성하고, 스레드를 실행시킨다. 자바 레벨에서 스레드를 추상화한다.

  • 유저 레벨 스레드가 시스템 콜을 통해서 커널 레벨 스레드를 사용하고 CPU 상에서 실행된다.

    • 유저 레벨 스레드는 반드시 커널 레벨 스레드와 연결되어야 한다.

2.3 매핑 모델 : 1:1, M:N, M:1

유저 레벨 스레드를 커널 레벨 스레드와 어떻게 연결하느냐에 따라 크게 두가지 모델로 나뉜다.

1:1 모델

  • 설명 : 유저 레벨 스레드 하나가 커널 레벨 스레드 하나와 직접 연결되는 방식이다.

  • 현재 : HotSpot JVM(JDK 1.2 부터) 등 대부분의 현대적인 JVM 이 이 모델을 채택한다.

  • 장점

    • 멀티코어 활용 극대화 : OS 가 각 스레드를 서로 다른 CPU 코어에 할당할 수 있어 병렬 처리에 매우 유리하다. (병렬성 확보!)

    • 단순한 구현 : 스레드 관리를 OS 에 맡기므로 JVM 구현이 비교적 간단하다.

    • 안정성 : 하나의 스레드가 입출력(I/O) 작업 등으로 잠시 멈춰도(Blocking), 다른 스레드는 OS 스케줄러에 의해 계속 실행될 수 있다.

  • 단점

    • 비싼 비용 : 자바 스레드를 만드는 것이 곧 OS 네이티브 스레드를 만드는 것이므로 생성 비용이 비싸다.

    • 개수 제한 : OS 가 생성할 수 있는 네이티브 스레드 수에 한계가 있어, 수만 개 이상의 스레드를 만들기는 어렵다.

M:1 모델

  • 설명 : 여러 유저 레벨 스레드가 하나의 커널 레벨 스레드와 연결돼 있는 것.

  • 장점

    • 여러개의 유저 레벨 스레드가 하나의 커널 레벨 스레드와 연결되어 있기에, 유저 레벨 스레드간 컨텍스트 스위칭이 일어날 시 1:1 모델보다는 적은 비용이 든다.

    • 스택 메모리 간 race condition 이 발생하지 않는다. 커널 레벨 스레드가 1개이기 때문이다.

  • 단점

    • 멀티 코어를 활용하지 못한다. 커널 레벨 스레드가 1개이기 때문이다.

    • 어느 1개의 유저 레벨 스레드에서 Blocking I/O 가 발생하면, 1개의 커널 레벨 스레드도 Blocking 상태가 되기 때문에 다른 유저 레벨 스레드도 모두 Blocking 상태가 된다.

  • Green Thread : JDK 1.2 까지는 M:1 모델을 사용했는데, 이때 이 유저 레벨 스레드들을 '그린 스레드(Green Thread)' 라고 명명했다.

M:N 모델

  • 설명 : M개의 유저 레벨 스레드(자바 스레드) 를 N개의 네이티브 스레드(OS 스레드) 에 할당하여 사용하는 방식이다. 보통 M 이 N 보다 훨씬 크다.

  • 장점

    • 가벼운 스레드 : 유저 레벨 스레드는 생성과 컨텍스트 스위칭이 매우 빠르고 가볍다.

    • 많은 스레드 생성 가능 : OS 의 제한 없이 이론상 매우 많은 수의 스레드를 생성할 수 있다.

  • 단점

    • 복잡한 구현 : JVM 스케줄러와 OS 스케줄러가 효율적으로 협력하도록 구현하기가 매우 어렵다.

2.4 가상 스레드가 왜 필요할까?

이전 설명에서 현재 자바는 1:1 매핑 모델을 사용한다고 했다.

이 방식은 안정적이지만, 두 가지 큰 단점이 있다.

  1. 비싸다 : OS 스레드를 만드는 작업은 OS 커널에 직접 요청해야 하므로(시스템 콜) 시간과 메모리 비용이 많이 든다. (스레드 하나당 약 1MB 의 스택 메모리 필요)

  2. 개수가 제한적이다 : OS 가 관리할 수 있는 스레드 수는 한정적이어서, 서버 하나에서 수천 개 이상의 스레드를 동시에 다루기 어렵다.

특히 요즘처럼 수 많은 클라이언트 요청을 동시에 처리해야 하는 웹 애플리케이션에서는 이게 큰 병목 지점이 된다. 예를 들어, 10,000 개의 요청을 동시에 처리하려면 10,000 개의 스레드가 필요한데, 플랫폼 스레드로는 불가능에 가깝다.

이 문제를 해결하기 위해서 '가상 스레드' 가 등장했다.

2.5 가상 스레드란 무엇인가?

JDK 1.2 부터 Java 21 이전까지 스레드는 기본적으로 1:1 매핑 방식을 채택하고 있었다. 이 방식은 유저 레벨 스레드가 커널 레벨 스레드와 1:1 매핑이 되기 때문에, 물리적인 한계로 하드위어 한계가 벗어나는 스레드를 사용하지 못한다는 한계점을 가지고 있었고, 일반적으로 요청량이 많은 웹서비스를 개발하는 Java 진영에서 해당 이슈를 개선하기 위해서 가상 스레드를 도입하게 되었다.

Java 19 등장한 방식으로, Java 21 에 정식 포함되었다. 가상 스레드의 목적은 다음과 같다.

  • 요청 당 스레드 구조의 애플리케이션의 HW 최적 사용 (여러개의 유저 레벨 스레드가 최대한 HW 최적화해 사용할 수 있도록 하자!)

  • 최소 변경으로 기존 코드에 가상 스레드 적용하기

  • 등등 ..

가상 스레드는 JVM 이 직접 관리하는 매우 가벼운 유저 레벨 스레드이다. OS 가 아닌 JVM 에 의해 생성되고 스케줄링 되기 때문에, 생성 비용이 거의 들지 않고 많은 수의 스레드를 생성할 수 있다.

가상 스레드의 핵심은 블로킹(blocking) 작업을 논블로킹(non-blocking) 처럼 처리한다는 것이다. -> 코드 자체는 동기인데, 실제 동작은 비동기처럼 동작한다.

여기서 말하는 블로킹 작업이란 대부분 I/O 작업을 의미하며, 아래와 같이 동작한다.

  1. 수많은 가상 스레드가 소수의 캐리어 스레드 위에서 실행된다. (M:N 모델)

  2. 가상 스레드 중 하나가 캐리어 스레드 위에서 실행되다가 DB 조회를 요청한다. 이 작업은 시간이 걸리는 I/O 블로킹 작업이다.

  3. 이때, JVM 은 캐리어 스레드를 멈추고 기다리게 하는 대신 다음과 같이 동작한다.

    1. DB 조회를 기다리는 가상 스레드의 작업 상태를 잠시 메모리에 저장하고, 캐리어 스레드에 내려놓는다.

    2. 이제 자유로워진 캐리어 스레드는 즉시 대기 중이던 다른 가상 스레드를 가져와 실행시킨다.

  4. 나중에 DB 조회가 끝나면, 해당 가상 스레드는 다시 실행 가능한 상태가 되어 비어있는 캐리어 스레드에 할당되기를 기다린다.

이러한 처리 방식으로 캐리어 스레드는 I/O 대기 시간 동안 낭비 없이 계속해서 다른 가상 스레드의 작업을 처리할 수 있다. 그 결과, 적은 수의 실제 스레드로 엄청나게 많은 동시 요청을 처리할 수 있게 되는 것이다.

가상 스레드 사용시 주의 점

  1. Pined : 가상 스레드와 캐리어 스레드가 고정되어서 캐리어 스레드가 다른 가상 스레드를 실행하지 못하는 상황이 발생한다.

    1. 상황1 : synchronized 블럭에서 I/O 이 발생하면 Pined 가 발생한다. -> 이 상황을 피하기 위해서 synchronized 블록이 아닌 Lock 을 사용해서 피해야 한다. -> 아직까지는 JDBC 드라이버를 가상 스레드와 함께 사용한다면 Pined 가 발생한다. (2025.08.12 기준) (사용하는 라이브러리 내부 로직 체크 필요)

    2. 상황2 : 네이티브 메서드 or foreign 함수 사용 시 발생

Last updated