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·라이브러리 점검
시리즈 다른 편
- 1편 — 동시성 기초·Java Thread
- 2편 — Carrier·Mount·Unmount
- 3편 — API·Builder·ExecutorService
- 4편 — Pinning·synchronized·ReentrantLock (현재 글)
- 5편 — Spring Boot 통합
- 6편 — Structured Concurrency
- 7편 — Performance·JFR·메모리
- 8편 — Patterns·실전·안티패턴
공식 문서: JEP 491 — Synchronize Virtual Threads 에서 더 깊이.
다음 글(5편)에서는 Spring Boot 통합 — Tomcat Virtual Thread·@Async·Spring MVC vs WebFlux 선택까지 풀어 갑니다.