Virtual Thread 마스터 — Pinning·synchronized·ReentrantLock

2026-05-03확률과 통계 마스터 노트

Java 21 Virtual Thread 마스터 노트 시리즈 4편. Pinning이 Virtual Thread의 가장 큰 함정인 이유, synchronized 블록·메서드가 unmount 차단하는 메커니즘, ReentrantLock·StampedLock 대안, JNI 호출 한계, 파일 I/O와 일부 라이브러리의 함정, Pinning 탐지 방법(jcmd·jstack)까지.

이 글은 Java 21 Virtual Thread 마스터 노트 시리즈의 네 번째 편입니다. Virtual Thread 도입의 가장 큰 함정 — Pinning.

synchronized 한 줄이 Virtual Thread 전체 효과를 무력화. ReentrantLock으로 대체. 이 차이가 운영 사고를 가르는 자리.

처음 Pinning이 어렵게 느껴지는 이유

처음 이 단원이 어렵게 느껴지는 이유는 두 가지예요. 첫째, "Pinning"이라는 단어가 낯섭니다. 둘째, synchronized가 왜 문제인지 직관적이지 않습니다. 표준 Java 문법인데?

해결법은 한 가지예요. "Pinning = 캐리어에 고정 = unmount X" 한 줄. I/O 호출해도 캐리어 못 떠남 → 다른 VT 못 받음 → Virtual Thread 효과 X. 이 본질만 잡으면 끝.

Pinning 정의

정상:
  VT-1 → I/O 대기 → unmount → C-1이 VT-2 받음
  
Pinning:
  VT-1 (synchronized 안) → I/O 대기 → unmount X
  C-1이 VT-1 점유 → 다른 VT 못 받음

Carrier Thread가 한 Virtual Thread에 묶임 = Pinning.

여기서 정말 중요한 시험 함정 — Pinning = Virtual Thread 효과 무력화. 캐리어 풀 (CPU 코어 수) 만큼만 동시 처리. 일반 Platform Thread와 같음.

원인 1 — synchronized

가장 흔한 함정

public class Counter {
    private int count = 0;
    
    public synchronized void increment() {
        count++;
        // I/O 호출 시 → Pinning!
        externalApi.call();
    }
}

synchronized 안에서 I/O = unmount X = Pinning.

synchronized 메서드 X

// 메서드 전체 synchronized
public synchronized void doWork() {
    // 안에서 I/O → Pinning
}

같은 문제.

해결 — ReentrantLock

public class Counter {
    private int count = 0;
    private final ReentrantLock lock = new ReentrantLock();
    
    public void increment() {
        lock.lock();
        try {
            count++;
            externalApi.call();   // unmount OK!
        } finally {
            lock.unlock();
        }
    }
}

ReentrantLock은 unmount 지원. Virtual Thread 친화.

여기서 정말 중요한 시험 함정 — 운영 코드 = synchronized → ReentrantLock 변환. Virtual Thread 도입 전 필수 점검. JDK 자체도 점진 변환 중.

원인 2 — JNI (Native Code)

public void useNative() {
    nativeMethod();   // JNI 호출
    // JNI 안에서 I/O → Pinning
}

네이티브 코드는 JVM이 unmount 못 함. 캐리어 점유.

해결 — 회피

JNI 호출이 짧으면 OK
JNI 안에서 I/O = Pinning, 별도 Platform Thread로

원인 3 — 일부 JDK 메서드 (점진 개선)

✗ Pinning (이전):
  - File·FileInputStream (Java 21 일부)
  - Object.wait() (Java 23 이전)
  - DatagramSocket (일부)

✓ Java 24+ — synchronized·Object.wait도 unmount (JEP 491)

Java 21 → 24+ 진화 중. 각 버전 release notes 확인.

Pinning 탐지

-Djdk.tracePinnedThreads

java -Djdk.tracePinnedThreads=full MyApp

# 또는 short
java -Djdk.tracePinnedThreads=short MyApp

Pinning 발생 시 stack trace 출력:

Thread[#21,VirtualThread,...]
    java.base/java.lang.VirtualThread$VThreadContinuation.onPinned
    java.base/java.lang.VirtualThread.parkOnCarrierThread
    java.base/java.lang.VirtualThread.park
    ...
    com.example.MyClass.method(MyClass.java:42)   ← 여기서 Pinning

여기서 시험 함정이 하나 있어요. tracePinnedThreads는 운영 환경 OFF 권장. 모든 Pinning 출력 = 로그 폭주. 개발·스테이징만.

jcmd·jstack

# 실행 중 스레드 스냅샷
jcmd <pid> Thread.print
jstack <pid>

# Carrier에 mount된 VT 보기

VT가 캐리어에 오래 mount = Pinning 의심.

Profiler

async-profiler·JFR로 Pinning hotspot 식별

ReentrantLock vs synchronized

측면 synchronized ReentrantLock
문법 키워드 API
Virtual Thread Pinning unmount OK
Try Lock (timeout) X O
Interruptible X O
Fair X O 옵션
Condition wait/notify Condition 객체
성능 (Platform Thread) 비슷 비슷

여기서 정말 중요한 시험 함정 — Java 24+ = synchronized도 unmount (JEP 491). 21에선 ReentrantLock, 24+에선 둘 다 OK. 마이그레이션 점진.

StampedLock·ReadWriteLock

StampedLock lock = new StampedLock();

long stamp = lock.readLock();
try {
    // 읽기
} finally {
    lock.unlockRead(stamp);
}

ReadWriteLock도 Virtual Thread 친화. 읽기·쓰기 분리.

Lock-free 자료구조

// AtomicInteger·ConcurrentHashMap·LongAdder
AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet();   // Lock-free, 항상 안전

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.computeIfAbsent("key", k -> 0);

Lock 자체 회피. 가장 효율.

라이브러리 Pinning 함정

✗ 옛 라이브러리 — synchronized 사용
  - 일부 JDBC 드라이버 (예: 옛 PostgreSQL 드라이버)
  - 옛 HTTP Client
  - 일부 Logger
  
✓ 최신 라이브러리 — ReentrantLock·Lock-free
  - PostgreSQL 42.7+
  - HikariCP 5.x+
  - SLF4J·Logback 최신

여기서 정말 중요한 시험 함정 — 운영 = 모든 의존성 점검. 옛 라이브러리 = Pinning 위험. 최신으로 업그레이드 또는 평가.

DB 연결 풀

// HikariCP (최신) — Virtual Thread 친화
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://...");
HikariDataSource ds = new HikariDataSource(config);

HikariCP 5.x+는 Virtual Thread 지원. ReentrantLock 사용.

여기서 시험 함정이 하나 있어요. Connection Pool 크기 변경. Virtual Thread = 수많은 동시 → 연결 풀 폭주. 권장:

  • 풀 크기는 DB가 견딜 만한 수
  • VT는 풀 대기 (자동 unmount)

File I/O 함정

// Java 21 기본 FileInputStream — Pinning 가능
try (var fis = new FileInputStream(file)) {
    fis.read();   // Pinning 위험
}

// Java NIO 권장
try (var fc = FileChannel.open(path)) {
    fc.read(buffer);   // unmount OK
}

여기서 시험 함정이 하나 있어요. Java 21 FileInputStream Pinning 알려진 이슈. NIO·Files API 권장.

Object.wait()·notify()

Java 23 이전 = Pinning. Java 24+ (JEP 491) = unmount OK.

// 21~23 — Pinning
synchronized (obj) {
    obj.wait();
}

// 대안 — Condition
ReentrantLock lock = new ReentrantLock();
Condition cond = lock.newCondition();
lock.lock();
try {
    cond.await();
} finally {
    lock.unlock();
}

마이그레이션 체크리스트

✓ synchronized → ReentrantLock 변환
✓ Object.wait/notify → Condition
✓ FileInputStream → NIO·Files
✓ JNI 호출 점검 (특히 I/O)
✓ 라이브러리 버전 업그레이드
✓ DB 연결 풀 (HikariCP 5+)
✓ jdk.tracePinnedThreads 개발 환경 활성
✓ Profiler로 hotspot 식별
✓ Java 24+ 업그레이드 (synchronized 자동)

CPU 집약 코드 — Pinning 아님

// CPU 집약 — Pinning ≠ 문제
Thread.startVirtualThread(() -> {
    for (int i = 0; i < 1_000_000_000; i++) {
        compute();
    }
});

여기서 정말 중요한 시험 함정 — CPU 집약 + VT = 효과 없음, but 문제도 X. 일반 Platform Thread와 같음. 다만 Virtual Thread 의미 없음 = ForkJoinPool·Platform Thread 권장.

실전 — Pinning 발견·수정

// 1. 개발 환경에서 -Djdk.tracePinnedThreads=full
// 2. 로그에서 Pinning stack trace 찾기
// 3. 해당 코드 수정
//    - synchronized → ReentrantLock
//    - 라이브러리 업그레이드
//    - 회피 (별도 Platform Thread)
// 4. 다시 측정

시험 직전 한 번 더 — 자주 헷갈리는 함정 모음

여기까지가 4편의 핵심입니다. 시험 직전 또는 실무에서 헷갈릴 때 다시 펼쳐 볼 수 있게 압축 노트로 마무리할게요.

  • Pinning = 캐리어에 고정·unmount X
  • Virtual Thread 효과 무력화
  • 원인 1 — synchronized (메서드·블록)
  • 원인 2 — JNI (Native Code)
  • 원인 3 — 일부 JDK API (점진 개선)
  • 해결 = ReentrantLock (try-finally)
  • ReentrantLock = unmount 지원·Virtual Thread 친화
  • Java 24+ JEP 491 = synchronized도 unmount
  • 21~23 = ReentrantLock 권장
  • -Djdk.tracePinnedThreads=full = Pinning 탐지
  • 운영 환경 OFF (로그 폭주)
  • jcmd·jstack·async-profiler로 추가 분석
  • ReentrantLock vs synchronized — Try Lock·Interruptible·Fair·Condition 추가
  • StampedLock·ReadWriteLock = 읽기/쓰기 분리
  • Lock-free (AtomicInteger·ConcurrentHashMap) = 가장 효율
  • 라이브러리 — HikariCP 5+·PostgreSQL 42.7+ Virtual Thread 친화
  • 옛 라이브러리 = Pinning 위험
  • DB 풀 크기 — DB 한계만큼·VT는 자동 대기
  • Java 21 FileInputStream = Pinning → NIO·Files
  • Object.wait/notify = Java 23 이전 Pinning → Condition 권장
  • 24+ = wait도 unmount
  • CPU 집약 = VT 의미 없음 (문제 X) — Platform Thread 권장
  • 마이그레이션 — synchronized·wait·File·JNI·라이브러리 점검

시리즈 다른 편

공식 문서: JEP 491 — Synchronize Virtual Threads 에서 더 깊이.

다음 글(5편)에서는 Spring Boot 통합 — Tomcat Virtual Thread·@Async·Spring MVC vs WebFlux 선택까지 풀어 갑니다.

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!