synchronization.2

ym56·2025년 11월 3일

OS

목록 보기
7/14

1. Definition

  • Mutual Exclusion (상호 배제; 화장실 변기 칸은 한 번에 한 명만 들어갈 수 있다. : 공유 자원에 대해 동시에 하나의 스레드만 접근하도록 제한하는 기법)

  • The Problem: 컴퓨터는 너무 빨라서, lock을 확인하고 1로 바꾸는 찰나의 순간에 다른 놈이 끼어든다..

  • The Solution: Atomic Instruction (원자적 명령어; 눈 깜짝할 새도 없이 순식간에 일어나는 일이라 아무도 끼어들 수 없다.. : 실행 도중 중단되거나 다른 연산이 끼어들 수 없는, 더 이상 쪼갤 수 없는 하드웨어 명령어)

이걸 모르면..

이 개념 없이 쇼핑몰 재고 관리 시스템을 만들면 Race Condition (경쟁 상태)이 발생한다.

  • 재고가 1개 남았는데, 고객 A와 고객 B가 동시에 "구매"를 누른다.

  • 락 (lock)이 제대로 작동 안 하면, 둘 다 "구매 성공"이 뜨고 물건은 1개인데 배송해야 할 물건은 2개가 된다. 회사는 망한다..


2. Code

(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를 태우고 있다는 뜻


3. 작동 원리

TestAndSet은 하드웨어까지 어떻게 내려갈까?

  1. Level 1: Java Code

    • lock.compareAndSet(false, true) 호출.
  2. Level 2: Bytecode (What JVM sees)

    • invokevirtual 명령어로 AtomicBoolean.compareAndSet 실행.
  3. Level 3: JVM Internals (C++)

    • OpenJDK의 Unsafe.cpp 파일로 이동한다.
    • 여기서 Atomic::cmpxchg (Compare and Exchange)라는 C++ 함수를 호출한다.
  4. Level 4: OS Kernel & Assembly

    • JVM은 운영체제나 CPU 아키텍처에 맞는 어셈블리어를 생성한다.
    • x86 (인텔/AMD) CPU의 경우:
    LOCK CMPXCHG [memory_address], reg
    • LOCK Prefix(락 접두어; CPU가 메모리로 가는 버스에 "통행 금지" 깃발을 꽂아버림)

      • 이 명령어가 실행되는 동안 다른 모든 CPU 코어는 메모리에 접근할 수 없다.

      • 이것이 바로 "하드웨어 레벨의 원자성"이다. 컨텍스트 스위칭? 불가. 하드웨어가 물리적으로 막아버렸기 때문.

  5. Level 5: Hardware

    • CPU의 ALU(산술논리연산장치)가 전압을 쏴서 메모리 값을 0에서 1로 바꾼다. 이 과정은 물리적으로 분할될 수 없다.

4. 활용

직접 스핀락을 만들기보다, 상황에 따라 스핀락과 대기(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는 전력을 아낌.


5. 문제 상황 (장애 대응)

상황: 서버 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. 해결 방안

  • 단기: 서버 재시작

  • 장기:

      1. try-finally 블록을 확인하여 예외가 터져도 반드시 락이 해제되도록 수정했는지 검증.
      1. 순수 스핀락 대신 ReentrantLock이나 synchronized로 교체. 이들은 일정 시간 스핀하다가 안 되면 스레드를 재우는 (Sleep/Block) 방식으로 전환하여 CPU를 아낀다.

6. 면접 대비 질문

Q: TestAndSet은 어떻게 원자성(Atomicity)을 보장하나?

  • Answer: 소프트웨어 레벨이 아닌 하드웨어, 즉 CPU 명령어 레벨에서 보장한다. 예를 들어 x86에서는 LOCK 접두어와 CMPXCHG 명령어를 사용하여, 해당 명령이 수행되는 동안 메모리 버스를 잠그거나 캐시 라인을 독점하여 다른 코어의 간섭을 물리적으로 차단한다.

Summary

  • TestAndSet (CAS): 읽고, 수정하고, 저장하는 3단계를 단 하나의 하드웨어 명령어로 압축한 것. 끼어들 틈이 없다.

  • Spinlock: 락 줄 때까지 집 안가 라고 떼 쓰기, 반응은 빠르지만 CPU를 엄청 낭비한다.

순수 스핀락을 위험하다. 잠깐만 스핀하고(Adaptive Spinning), 안 되면 자러 가는(Block) 하이브리드 방식(Java Lock)을 쓴다


0개의 댓글