
백엔드에서 동시성 문제는 결국 Race Condition, Deadlock, Throughput 저하, GC 압박 증가로 이어집니다.
오늘은 반드시 알아야 할 주제인 synchronized에 대해 알아보겠습니다.
모든 객체는 메모리에서 다음과 같은 구조를 가집니다.
| Mark Word | Klass Pointer | (Padding) | Instance Data |
여기서 핵심은 Mark Word입니다.
Mark Word에 들어가는 정보는 HashCode, GC age, Lock 상태, Thread ID입니다.
monitor에 진입하며 아래의 코드는
synchronized (lock) {
// critical section
}
아래의 바이트 코드로 변환됩니다.
monitorenter
...
monitorexit
의미는 다음과 같습니다.
락을 건다 = monitorenter 명령어 실행
락을 푼다 = monitorexit 명령어 실행
여기서 Monitor는 JVM 내부의 동기화 구조입니다.
monitorenter 실행 시,
1. 객체의 Mark Word 확인
2. 현재 소유 스레드 확인
3. CAS로 소유권 획득 시도
4. 실패 시 대기 큐 진입
Biased Lock은 JDK 15부터 기본 비활성화, JDK 21에서는 완전히 제거되었습니다.
따라서 요즘 환경인 Java 17, 21 환경에서는 다음 두 단계만 고려하면 됩니다.
경쟁이 발생하면, Lightweight 단계가 Heavyweight 단계로 승격됩니다. 한 번 Heavyweight로 승격되면, 다시 Lightweight로 돌아가지 않습니다.
Heavyweight Lock 상태에서는 스레드 Block 증가, 객체 생존 시간 증가, Old Gen 승격 가능성 증가, GC Pause 시간 증가 현상이 발생합니다. 따라서 락 설계는 GC 설계와도 연결됩니다.
synchronized는 단순한 키워드가 아니라, 객체 헤더의 Mark Word를 변경하는 JVM 레벨의 동작이며, 경쟁이 발생하면 OS Mutex 기반의 Heavyweight Lock으로 승격되어 컨텍스트 스위칭 비용을 유발하고, Virtual Thread 환경에서는 carrier thread pinning 문제를 초래할 수 있으며, 잘못 설계될 경우 전체 TPS 저하와 GC 부담 증가로까지 이어질 수 있습니다.