백엔드 면접 용어 사전
Java·객체지향Synchronize Virtual Threads without Pinning

JEP 491

Java 24에서 synchronized로 인한 가상 스레드 pinning을 없앤 변경. 모니터 소유권을 캐리어가 아닌 가상 스레드로 추적한다.

Java 24에 들어간 변경으로, synchronized 때문에 생기던 가상 스레드 pinning을 제거했다. JEP는 JDK Enhancement Proposal의 약자로, 자바에 들어가는 변경 하나하나에 붙는 번호다.

무엇을 고쳤나 — 소유권을 누구 이름으로 기록할 것인가

synchronized가 잡는 모니터는 "이 블록에는 한 번에 하나만 들어간다"를 보장하는 장치다. JVM은 그 모니터가 지금 누구 것인지를 어딘가에 기록해야 하는데, 그 기준이 바뀐 것이 이 변경의 전부다.

[Java 21~23]

  • 모니터 소유자 = 캐리어 스레드(OS 스레드)의 정체성
  • 가상 스레드를 다른 캐리어로 옮기면 소유자가 바뀐 것처럼 보인다
  • 옮길 수 없다 → 캐리어에 고정(pinning)

[Java 24+]

  • 모니터 소유자 = 가상 스레드 자신의 정체성
  • 어느 캐리어 위에 있든 소유자는 그대로
  • 자유롭게 언마운트·재마운트 가능

이를 위해 HotSpot(오라클 JDK·OpenJDK의 JVM 구현)의 모니터 구현을 가상 스레드를 인지하도록 다시 썼다.

무엇이 가능해졌나

동작21~2324+
synchronized 안에서 블로킹 I/O캐리어 고정언마운트됨
synchronized 안에서 Object.wait()캐리어 고정언마운트됨
모니터 재획득 대기캐리어 고정언마운트됨

왜 이게 판을 바꿨나

가상 스레드 도입을 실제로 막던 게 이거였다. 드라이버·커넥션 풀·로깅 라이브러리 내부의 synchronized내가 고칠 수 없는 남의 코드라, 라이브러리 생태계 전체가 바뀌기를 기다려야 했다. JEP 491은 그 요구를 JVM 한 곳에서 해결했다.

부수적으로 이 변경은 리액티브를 고수할 근거 하나를 지웠다. "가상 스레드로 DB를 때리면 결국 pinning으로 막힌다"가 R2DBC·WebFlux를 유지할 실질적 이유였는데, 그게 사라졌다.

남은 pinning

네이티브 프레임(JNI·FFM), 클래스 로딩·정적 초기화, 리눅스 로컬 파일 I/O 셋은 여전히 캐리어를 고정한다.

면접 포인트

"가상 스레드에서 synchronized가 문제인가?"라는 질문에 버전을 나눠서 답하면 확실히 구분된다 — 21~23에서는 그랬고, 24의 JEP 491에서 제거됐으며, 네이티브 프레임·클래스 초기화·파일 I/O만 남았다.

실제로 무엇이 달라지나

// 라이브러리 내부에 흔한 패턴 — 내가 쓴 코드가 아니어도 걸린다
synchronized (connectionPool) {
    return dataSource.getConnection();     // 여기서 블로킹
}
  • [Java 21~23] — 이 블록에 들어간 가상 스레드는 캐리어에 고정된다

    • 캐리어가 8개(코어 수)뿐이라 8개가 물리면 전체가 멈춘다
  • [Java 24+] — 언마운트되어 캐리어를 놓아 준다

    • 같은 코드, 같은 라이브러리, 재컴파일도 불필요

pinning이 있었는지 확인하는 법

# Java 21~23: pinning 발생 시 스택 트레이스를 찍어 준다
java -Djdk.tracePinnedThreads=full -jar app.jar

여기서 남의 라이브러리 이름이 찍히면 손쓸 방법이 없었다. JEP 491이 중요한 이유가 이것이다 — 생태계 전체가 바뀌기를 기다리는 대신 JVM 한 곳에서 해결했다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 모던 자바·스프링 — 가상 스레드·JEP 491·리액티브·jOOQ·Boot 4