배경
Java 21에서 소개된 가상 스레드(Virtual Thread)는 수많은 동시 작업을 처리할 수 있는 경량화된 스레드로, 고성능 동시성 애플리케이션을 개발하기 위한 중요한 전환점이 되었습니다.
가상 스레드는 JDK 자체 스케줄러를 통해 플랫폼 스레드에 마운트되었다가 필요에 따라 해제되면서 효율적인 리소스 관리를 지원합니다.
그러나 Java 21에서는 Virtual Thread Pinning(핀) 문제가 성능 개선에 영향을 미치고 있습니다. 특정 상황에서 가상 스레드가 플랫폼 스레드를 고정되어 가상 스레드의 주된 이점인 플랫폼 스레드 마운트 언마운트를 제한하는 케이스가 종종 있습니다.
왜 Pinning이 발생하나?
가상 스레드는 기본적으로 논블로킹 작업을 수행할 때 플랫폼 스레드에서 해제되어야 하지만, 특정 동기화 및 블로킹 작업 시 플랫폼 스레드에 고정되는 문제가 있습니다.
주요 발생 원인은 다음과 같습니다
1. synchronized 메서드와 Pinning
synchronized는 JVM 모니터(Monitor)를 사용하여 스레드 간 상호 배제를 보장합니다.
모니터란?
Java에서 모든 객체는 고유한 모니터(Monitor)를 가지고 있습니다. 모니터는 Java에서 스레드 동기화를 구현하는 핵심 메커니즘으로, 동기화 블록이나 동기화 메서드를 사용할 때 자동으로 생성됩니다. 특정 객체를 기반으로 스레드 간 상호 배제(Mutual Exclusion)와 상태 동기화(Condition Synchronization)를 제공하여 공유 자원의 안전한 접근을 보장합니다.
- 동기화 블록: synchronized 키워드로 정의되는 동기화 블록은 객체의 모니터를 획득(acquire)하고 해제(release)하여 특정 코드 블록이나 메서드에 단일 스레드만 접근할 수 있도록 보장합니다.
- wait/notify 메서드: 스레드가 모니터를 사용해 다른 스레드와 상태를 동기화할 수 있도록 합니다. (wait()는 잠금 해제 후 대기, notify()는 대기 중인 스레드 깨우기)
JVM은 모니터를 플랫폼 스레드 기준으로 관리합니다. 가상 스레드가 synchronized 메서드에 진입하면, 모니터의 소유권은 가상 스레드가 아니라 가상 스레드의 캐리어 플랫폼 스레드에 할당됩니다. 이 상태에서 가상 스레드가 I/O 등의 블로킹 작업을 수행하면 플랫폼 스레드는 해제되지 않고 고정(Pinned)됩니다.
synchronized void fetchData() {
byte[] data = new byte[1024];
socket.getInputStream().read(data); // 블로킹 작업
}
위 코드에서 read 메서드가 데이터를 대기하며 블로킹되면, 가상 스레드는 플랫폼 스레드에 고정되어 다른 가상 스레드를 처리하지 못하는 상태가 됩니다.
라이브러리 Pinning 사례
Hibernate/JPA
Spring Data JPA 3.3.0 버전에서 PartTreeJpaQuery.QueryPreparer#createQuery() 메서드의 synchronized 블록으로 인해 virtual thread pinning이 발생한다는 이슈가 보고되었습니다. 이 문제를 해결하기 위해 synchronized 블록을 ReentrantLock으로 교체하는 것이 제안되었습니다.
https://github.com/spring-projects/spring-data-jpa/issues/3505
HikariCP
반면 HikariCP는 syncronized 블럭 사용으로 인한 pinning 문제를 해결하지 않기로 결정했습니다.
Virtual Thread "pinning" 문제는 특정 조건에서 발생하는데, 이는 Virtual Thread가 synchronized 블록 내부에서 IO 작업 또는 블로킹 작업을 수행할 때 발생합니다. 하지만 HikariCP는 이러한 블로킹 작업을 synchronized 내부에서 수행하지 않습니다.
ReentrantLock을 사용하도록 변경하는 것은 Virtual Threads의 호환성을 위한 시도로 제안되었으나, HikariCP의 기존 synchronized 사용 방식에서 실질적인 성능 개선이나 문제 해결 효과가 거의 없을 가능성이 크며, ReentrantLock으로의 변경은 불필요한 오버헤드(추가 객체 생성 및 GC)를 초래할 수 있다고 Brett Wooldridge(HikariCP의 소유자)가 언급했습니다.
https://github.com/brettwooldridge/HikariCP/pull/2055
2. Object.wait()와 Pinning
Object.wait()는 동기화된 객체에서 대기 상태로 전환될 때 사용하는 메서드입니다.
작동 방식
- Object.wait()는 모니터를 소유한 상태에서 호출해야 합니다.
- 호출된 스레드는 대기 상태로 전환되며, 모니터를 해제.
- 다른 스레드가 Object.notify() 또는 Object.notifyAll()을 호출하면 대기 상태에서 깨어남.
Pinning이 발생하는 이유
- wait 호출 중에도 가상 스레드는 플랫폼 스레드와 연결되어 있습니다.
- 깨어난 후 다시 모니터를 재획득해야 하는데, 이 과정에서도 플랫폼 스레드가 고정됩니다.
Java 24 의 해결 방안 - JEP491
Java 24의 JEP 491은 Virtual Thread Pinning 문제를 해결하기 위해 JVM 수준의 동기화 메커니즘을 대폭 개선했습니다. 이 개선은 가상 스레드가 synchronized 메서드, 블록, 또는 Object.wait() 호출 중에도 플랫폼 스레드에서 분리(Mount 해제)될 수 있도록 지원합니다. 이를 통해 가상 스레드가 블로킹 작업을 수행하는 동안 플랫폼 스레드가 유휴 상태로 고정되지 않게 되어 확장성이 크게 향상됩니다.
1. Pinning 문제의 핵심 원인
기존 JVM의 동작 방식
- synchronized 키워드와 모니터(Monitor)
- synchronized는 JVM 내부적으로 객체의 모니터를 활용하여 상호 배제를 보장합니다.
- JVM은 특정 스레드(현재는 플랫폼 스레드)가 모니터를 소유하고 있음을 추적합니다.
- 가상 스레드가 synchronized 메서드에 진입하면, JVM은 해당 가상 스레드의 캐리어 플랫폼 스레드를 모니터 소유자로 설정합니다.
- 이 상태에서 가상 스레드가 블로킹 작업에 들어가더라도 플랫폼 스레드는 모니터와 연결된 상태로 고정됩니다.
- Object.wait()
- Object.wait()는 모니터를 소유한 상태에서 호출해야 합니다.
- 호출된 스레드는 대기 상태로 전환되며 모니터를 일시적으로 해제합니다.
- 그러나 JVM은 여전히 플랫폼 스레드를 대기 상태로 유지하므로, 플랫폼 스레드가 다른 작업에 재사용되지 못합니다.
2. JEP 491의 해결 방안
Java 24는 Pinning 문제를 해결하기 위해 다음과 같은 변경을 도입했습니다.
1) 모니터 소유권을 가상 스레드 기준으로 변경
- 기존 JVM은 모니터 소유권을 플랫폼 스레드 기준으로 관리했지만, Java 24에서는 이를 가상 스레드 기준으로 관리합니다.
- 가상 스레드가 synchronized 메서드에 진입하면
- JVM은 해당 가상 스레드를 모니터 소유자로 설정합니다.
- 플랫폼 스레드와는 독립적으로 모니터 소유권을 유지할 수 있습니다.
- 이를 통해 가상 스레드가 블로킹 상태에 들어가더라도 플랫폼 스레드가 고정되지 않고 다른 가상 스레드에 재사용될 수 있습니다.
2) Object.wait()의 동작 개선
- Object.wait() 호출 시
- 가상 스레드는 플랫폼 스레드에서 Unmount됩니다.
- 대기 상태가 끝나면 JVM 스케줄러는 가상 스레드를 새로운 플랫폼 스레드에 Mount하여 작업을 재개합니다.
- 이를 통해 wait 대기 중에도 플랫폼 스레드가 유휴 상태로 고정되지 않습니다.
'Java & Kotlin' 카테고리의 다른 글
[Kotlin] Coroutine Flow로 스타크래프트2 프로토스 연결체 시간증폭 구현하기 (2) | 2024.11.03 |
---|---|
[Java/Kotlin] 정수 비교와 객체 캐싱: 1 === 1, 128 === 128의 결과는? (0) | 2024.09.20 |
[Kotlin] Nullable Value Class 이슈 - JPA Entity (1) | 2024.02.06 |
[Kotlin] 제네릭스(Generics) - 불공변, 공변, 반공변 (1) | 2023.09.06 |
[Java] HotSpot VM의 Survivor영역은 왜 2개일까? (0) | 2023.08.28 |