Mutual Exclusion (상호 배제; 화장실 변기 칸은 한 번에 한 명만 들어갈 수 있다. : 공유 자원에 대해 동시에 하나의 스레드만 접근하도록 제한하는 기법)
The Problem: 컴퓨터는 너무 빨라서, lock을 확인하고 1로 바꾸는 찰나의 순간에 다른 놈이 끼어든다..
The Solution: Atomic Instruction (원자적 명령어; 눈 깜짝할 새도 없이 순식간에 일어나는 일이라 아무도 끼어들 수 없다.. : 실행 도중 중단되거나 다른 연산이 끼어들 수 없는, 더 이상 쪼갤 수 없는 하드웨어 명령어)
이걸 모르면..
이 개념 없이 쇼핑몰 재고 관리 시스템을 만들면 Race Condition (경쟁 상태)이 발생한다.
재고가 1개 남았는데, 고객 A와 고객 B가 동시에 "구매"를 누른다.
락 (lock)이 제대로 작동 안 하면, 둘 다 "구매 성공"이 뜨고 물건은 1개인데 배송해야 할 물건은 2개가 된다. 회사는 망한다..
(Spinlock in Java)
import java.util.concurrent.atomic.AtomicBoolean;
public class SimpleSpinLock {
// 공유되는 잠금 장치 (AtomicBoolean은 CPU의 원자적 명령을 사용한다)
private final AtomicBoolean lock = new AtomicBoolean(false);
public void criticalSection() {
// 진입 시도 (Spinlock: 문이 열릴 때까지 계속 문고리를 돌린다)
while (!lock.compareAndSet(false, true)) {
// Busy-Waiting: 계속 돌면서 확인 중.. (CPU 사용)
}
try {
// Critical Section (임계 구역) : 나만 들어올 수 있는 방
System.out.println(Thread.currentThread().getName() + "is working..");
} finally {
// 잠금 해제 (볼일 다 봤으니 문 열어둠)
lock.set(false);
}
}
}
lock.compareAndSet(false, true)
"지금 문이 열려있니? (false) 그렇다면 내가 닫고 들어갈게 (true). 이 질문과 행동을 쉴 틈도 없이 한 번에 처리한다.
Return: 성공하면 true, 실패하면(누가 이미 잠갔으면) false 반환.
while(!...) (Spinlock 핵심)false) !에 의해 true가 되어 while 루프를 돈다. 즉, 성공할 때까지 무한히 재시도한다. 이것이 바로 스핀락.리눅스 환경
이 코드가 서버에서 돌면 실제로 어떤 일이 일어날까? 스핀락은 CPU를 갉아먹는 주범이 될 수 있다..
서버가 느려졌을 때, 어떤 스레드가 범인인지 잡는 명령어
top -H -p [JAVA_PID]
H: 프로세스뿐만 아니라 내부의 스레드까지 보여줘라
-p: 특정 자바 프로세스만 찍어서 봐라
Output
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
8001 backend 20 0 4.5g 512m 128m R 99.9 1.2 0:45.12 java-Thread-1
8002 backend 20 0 4.5g 512m 128m S 0.1 1.2 0:12.33 java-Thread-2
PID 8001: 현재 스핀락을 획득하려고 while문을 미친 듯이 돌고 있는 스레드
%CPU 99.9: 스핀락은 대기하는 동안 "잠(Sleep)"을 자지 않는다. 계속 깨어서 "문 열렸나?" 확인하므로 CPU를 100% 사용한다. 이것이 스핀락의 치명적 단점..
S (Status): R(Running). 멈추지 않고 계속 CPU를 태우고 있다는 뜻
TestAndSet은 하드웨어까지 어떻게 내려갈까?
Level 1: Java Code
lock.compareAndSet(false, true) 호출.Level 2: Bytecode (What JVM sees)
invokevirtual 명령어로 AtomicBoolean.compareAndSet 실행.Level 3: JVM Internals (C++)
Unsafe.cpp 파일로 이동한다.Atomic::cmpxchg (Compare and Exchange)라는 C++ 함수를 호출한다.Level 4: OS Kernel & Assembly
LOCK CMPXCHG [memory_address], reg
LOCK Prefix(락 접두어; CPU가 메모리로 가는 버스에 "통행 금지" 깃발을 꽂아버림)
이 명령어가 실행되는 동안 다른 모든 CPU 코어는 메모리에 접근할 수 없다.
이것이 바로 "하드웨어 레벨의 원자성"이다. 컨텍스트 스위칭? 불가. 하드웨어가 물리적으로 막아버렸기 때문.
Level 5: Hardware
직접 스핀락을 만들기보다, 상황에 따라 스핀락과 대기(Sleep)을 섞어 쓰는 ReentranrLock이나 라이브러리가 있다.
스핀락의 개념을 살려 "짧은 대기에는 스핀락을 쓰고, 길어지면 포기하는" 코드를 짜보자.
초단타 매매 시스템. 락을 얻기 위해 오래 기다리면 손해고, 아주 잠깐만 기다렸다가 안 되면 바로 다른 작업을 해야 한다.
import java.util.concurrent.atomic.AtomicBoolean;
public class TradingSystem {
private final AtomicBoolean lock = new AtomicBoolean(false);
public boolean tryTrade() {
// 최대 1000번만 시도해보고 안되면 포기 (CPU 낭비 방지)
int spinCount = 0;
int MAX_SPIN = 1000;
// 1. Spinlock 시도
while (!lock.compareAndSet(false, true)) {
spinCount++;
if (spinCount > MAX_SPIN) {
// 2. 너무 오래 걸리면 포기
System.out.println("락 획득 실패: 서버 혼잡");
return false;
}
// 3. CPU에게 힌트 주기
Thread.onSpinWait();
}
try {
// 4. Critical Section
System.out.println("매매 체결 진행 중..");
return true;
} finally {
lock.set(false); // 락 해제
}
}
}
while (!lock.compareAndSet...): "문 열렸어?"라고 계속 물어보기. 하드웨어의 CMPXCHG 명령어 사용.
spinCount > MAX_SPIN: 무작정 기다리면 CPU가 100%로 타버리니, 1000번만 물어보고 안 되면 "다음에 다시 올게"하고 빠져나간다. 이것이 Bounded Spinlock.
Thread.onSpinWait(): CPU에게 "나 지금 헛바퀴 돌고 있어 (Spinning)"라고 알려준다. CPU는 전력을 아낌.
상황: 서버 CPU가 갑자기 100%를 찍고 내려오지 않는다.. 요청 처리는 멈춰있따.
1. 증거 수집(top & jstack)
top을 보니 Sys CPU는 낮은데 User CPU가 100%이다. (만약 락 경합으로 인한 Context Switching이 원인이라면 Sys CPU가 높았을 것이다. User CPU가 높다는 건 스핀락이 범인이라는 강력한 증거이다.)
jstack으로 스레드 덤프를 뜬다.
"Thread-5" #23 prio=5 os_prio=0 tid=0x00... runnable
java.lang.Thread.State: RUNNABLE
at com.mycompany.SimpleSpinLock.criticalSection(SimpleSpinLock.java:10)
RUNNABLE 상태인데 멈춰있다. while 루프 안이다.
원인: 누군가 락을 잡고 lock.set(false)를 하지 않은 채 죽었거나(예외 발생), 락을 잡은 스레드가 너무 느려서 다른 스레드들이 무한히 회전(Spinning)하고 있는 것이다.
2. 해결 방안
단기: 서버 재시작
장기:
try-finally 블록을 확인하여 예외가 터져도 반드시 락이 해제되도록 수정했는지 검증.ReentrantLock이나 synchronized로 교체. 이들은 일정 시간 스핀하다가 안 되면 스레드를 재우는 (Sleep/Block) 방식으로 전환하여 CPU를 아낀다.Q: TestAndSet은 어떻게 원자성(Atomicity)을 보장하나?
LOCK 접두어와 CMPXCHG 명령어를 사용하여, 해당 명령이 수행되는 동안 메모리 버스를 잠그거나 캐시 라인을 독점하여 다른 코어의 간섭을 물리적으로 차단한다.TestAndSet (CAS): 읽고, 수정하고, 저장하는 3단계를 단 하나의 하드웨어 명령어로 압축한 것. 끼어들 틈이 없다.
Spinlock: 락 줄 때까지 집 안가 라고 떼 쓰기, 반응은 빠르지만 CPU를 엄청 낭비한다.
순수 스핀락을 위험하다. 잠깐만 스핀하고(Adaptive Spinning), 안 되면 자러 가는(Block) 하이브리드 방식(Java Lock)을 쓴다
