Lock

MONA·2025년 4월 28일

나혼공

목록 보기
67/92

Lock

여러 작업(스레드, 프로세스 등)이 동시에 공유자원에 접근할 때, 한 번에 하나만 접근할 수 있도록 제어하는 매커니즘

Lock의 목적

  • 데이터 무결성 보호
  • Race Condition(경쟁 상태) 방지
  • 동시성 문제 해결

Lock이 필요한 이유

멀티스레스 환경에서는 여러 스레드가 동시에 같은 데이터를 조작할 수 있다. 이 때 순서에 따라 결과가 달라지는 문제(Race Condition)가 발생할 수 있기 때문에, 한 번에 하나의 스레드만 작업하도록 막아야 한다.

Lock 동작 흐름

  1. 스레드 A가 공유 자원에 접근 시도
  2. Lock을 확인
    • Lock이 없는 경우: Lock을 걸고 접근
    • Lock이 있는 경우: 대기
  3. 스레드 A가 작업을 완료하면 Lock 해제
  4. 다음 스레드 접근

Lock의 종류

종류설명예시
Mutual Exclusion(상호 배제)가장 기본적인 락. 한 번에 하나만 접근 가능synchronized, ReentrantLock
Read-Write Lock읽기는 여러 개 허용, 쓰기는 하나만 허용ReentrantReadWriteLock
Optimistic Lock(낙관적 락)락 안 걸고 먼저 작업한 후 충돌시 실패 처리DB에서 버전 필드(version)를 사용
Pessimistic Lock(비관적 락)접근 자체를 막아 충돌 방지DB 트랜젝션에서 SELECT FOR UPDATE

제어 방식 기준

  1. Pessimistic Lock(비관적 락)

    • 문제가 일어날 것을 예상하고 미리 락을 걸어서 다른 작업을 막는 방식
    • 공격적 락: 자원 접근 전 무조건 락을 건다.
    • 충돌이 많이 발생할 경우에 유리
    • DB에서는 SELECT FOR UPDATE 구문으로 구현
    • 충돌 방지가 확실하나 락을 걸고 기다리는 비용(대기시간)이 큼
    SELECT * FROM orders WHERE id = 1 FOR UPDATE;
  2. Optimistic Lock(낙관적 락)

    • 문제가 별로 없을것이라 예상하고 일단 접근, 나중에 검증하는 방식
    • 락을 걸지 않고 작업 수행, 커밋 전에 version 등을 확인해서 충돌 검사
    • 충돌이 적을 때 성능이 좋음
    • 락이 없어서 빠르지만 충돌 시 재시도 필요
    if (currentVersion == dbVersion) {
        update();
    } else {
        throw new OptimisticLockingFailureException();
    }

접근 방식 기준

  1. Mutex, Mutual Exclusion (상호배제 락)

    • 한 번에 하나만 자원을 사용할 수 있게 막음
    • 다른 스레드는 기다려야 함
    • Java의 synchronized, ReentrantLock이 대표적
  2. Shared Lock, Read Lock (공유 락)

    • 읽기는 동시에 여러 스레드가 가능, 쓰기는 불가능한 락
    • 주로 읽기가 많은 환경에서 사용
    • Java의 ReentrantReadWriteLock의 readLock()
  3. Exclusive Lock, Write Lock (배타 락)

    • 쓰기 작업은 오직 하나만 가능한 락
    • 다른 모든 접근을 차단
    • ReentrantReadWriteLock의 writeLock()

스레드 간 정책 기준

  1. Fair Lock (공정 락)

    • 락을 요청한 순서대로 락을 부여하는 락
    • 대기 순서 보장
    • 성능이 떨어질 수 있음(context switching 빈번)
    ReentrantLock lock = new ReentrantLock(true); // 공정 락
  2. Non-Fair Lock (비공정 락)

    • 순서를 무시. 락이 빨리 풀리면 기다리던 스레드보다 다른 스레드가 락을 먼저 가져갈 수 있음
    • 대기 시간은 불확실하지만 전체 처리량은 높을 수 있음
    ReentrantLock lock = new ReentrantLock(false); // 비공정 락

그 외

  1. Spin Lock (스핀 락)

    • 락이 풀릴 때까지 계속 CPU를 소비하며 반복해서 락을 시도하는 락
    • 잠깐 대기할 때 유리(락 점유 시간이 짧은 경우)
    • 오래 걸리면 비효율적(CPU 낭비)
  2. 세마포어 (Semaphore)

    • 한정된 개수만 접근 허용
    • Java에서는 Semaphore 클래스로 제공
    Semaphore semaphore = new Semaphore(5); // 5개까지 동시에 접근 허용
  3. Read-Write Lock

    • 읽기는 여러 스레드가 가능, 쓰기는 하나만 가능인 복합 락
    • 읽기 작업이 많은 시스템에서 성능 최적화에 유리
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    lock.readLock().lock(); // 읽기 락
    lock.writeLock().lock(); // 쓰기 락

정리

종류특징사용 예시
비관적 락무조건 락, 충돌 방지DB SELECT FOR UPDATE
낙관적 락락 없이 작업, 나중 검증버전 필드 검사
상호 배제 락한번에 하나만 접근synchronized, ReentrantLock
공유 락읽기 동시 허용ReentrantReadWriteLock.readLock()
배타 락쓰기 단독 허용ReentrantReadWriteLock.writeLock()
공정 락요청 순서 보장ReentrantLock(true)
비공정 락순서 무시, 속도 빠름ReentrantLock(false)
스핀 락대기 중 락 계속 시도짧은 락 점유 환경
세마포어동시 접근 개수 제한커넥션 풀
리드-라이트읽기 다수, 쓰기 단일다중 읽기 서비스

Lock 사용 시 주의점

  • DeakLock(교착 상태): 서로 락 해제를 기다리면서 무한 대기하는 상황
  • 성능 이슈: 락 경쟁이 심해지면 시스템 속도에 영향을 줄 수 있음
  • 반드시 finally 블록에서 unlock() 호출하기: 예외 발생 시에도 락이 풀리지 않는 경우 시스템이 멈출 수 있음

DeadLock (교착 상태)

  • 여러 스레드가 서로 락을 점유한 채, 상대방의 락을 기다리면서 무한 대기하는 상태
  • ex) A 스레드는 B가 가진 락이 해제되기를 기다리고, B 스레드는 A가 가진 락이 풀리기를 기다리는 상황 -> 아무 일도 진행되지 않고 시스템이 멈추게 됨
public class DeadlockExample {

    private final Object lockA = new Object();
    private final Object lockB = new Object();

    public void method1() {
        synchronized (lockA) {
            System.out.println("Thread 1: lockA 획득");
            synchronized (lockB) {
                System.out.println("Thread 1: lockB 획득");
            }
        }
    }

    public void method2() {
        synchronized (lockB) {
            System.out.println("Thread 2: lockB 획득");
            synchronized (lockA) {
                System.out.println("Thread 2: lockA 획득");
            }
        }
    }
}

Thread 1lockA를 잡고 lockB를 기다림
Thread 2lockB를 잡고 lockA를 기다림
서로 상대방의 락 해제를 기다리며 무한 대기

Deadlock이 발생하는 조건

  • 상호배제: 하나의 리소스는 하나의 프로세스만 사용 가능
  • 점유 및 대기: 락을 잡은 상태로 다른 락을 기다림
  • 비선점: 락을 강제로 뺏을 수 없음
  • 순환 대기: 프로세스들이 서로 락을 점진적으로 대기

위의 네 가지 조건이 충족되면 Deadlock이 발생할 수 있음

Deadlock을 예방하기 위해서

  • lock에 대해 획득 순서 정의(lockA 획득 후 lockB 획득 순서 고정)
  • 타임아웃 설정(일정 시간 내에 락을 못 얻으면 포기)
  • Deadlock 감지 알고리즘 사용

Starvation (기아 상태)

  • 락을 획득할 기회가 계속 박탈되어 특정 스레드가 영원히 실행되지 못하는 상태
  • 다른 우선순위가 높은 스레드가 CPU나 리소스를 점거하면 낮은 우선순위 스레드는 계속 대기하다가 실행 기회를 얻지 못하는 경우

Starvation 발생 예시

  • Priority Scheduling에서 낮은 우선순위 프로세스가 무한 대기
  • 락을 공정하게 나누지 않고 빠른 스레드가 계속 먼저 락을 잡는 경우(공정 락이 아닌 비공정 락을 사용할 경우)

Starvation을 예방하기 위해서

  • 공정 락 사용
ReentrantLock lock = new ReentrantLock(true) // 요청 순서대로 락 획득
  • 우선순위 조정: 오래 기다린 스레드의 우선순위를 점차 높이는 에이징(Aging) 기법 적용

정리

락은 동시성 문제를 해결하기 위한 도구로, 상황에 따라 비관적/낙관적, 공유/배타, 공정/비공정 락 등을 선택해서 사용한다

profile
고민고민고민

0개의 댓글