[Java] 동시성 제어 1탄 - Lock

2025. 12. 27. 18:31·Teck Stack/Java

개요


멀티 스레딩 환경에서 발생할 수 있는 동시성 문제 상황을 살펴보고, Java에서 이를 어떻게 제어할 수 있는지 알아보고자 합니다.

 

동시성 제어에는 다양한 방법이 존재하지만, 이번 포스팅에서는 그중에서도 Lock을 활용한 제어 방식에 대해 중점적으로 다뤄보고자 합니다.

 

 

 

동시성 문제 상황


 

상품 클래스(Product)에 재고 감소 로직(decrease)이 있습니다.

public class Product {
    private String name;
    private int stock;

    public Product(String name, int stock) {
        this.name = name;
        this.stock = stock;
    }

    public void decrease() {
        this.stock = this.stock - 1;
    }

	...
}

 

상품을 구매하는 경우 decrease() 메서드로 상품의 재고를 감소시킵니다.

 

하지만 멀티 스레드 환경에서 해당 재고 감소 로직이 동시에 많이 호출된다면, Race Condition이 발생할 수 있습니다.

 

Race Condition은 여러 스레드가 동시에 공유 자원에 접근할 때, 접근 순서에 따라 결과가 달라지는 상황을 의미합니다.

 

 

 

다음 테스트 코드를 통해, 실제로 경쟁 상태가 발생하는지 확인해 보겠습니다.

@Test
    void decrease() throws InterruptedException {
        Product product = new Product("banana", 10000);
        Thread[] threads = new Thread[100];

        for (int i = 0; i < 100; i++) {
            Thread t = new Thread(() -> {
                for (int j = 0; j < 100; j++) {
                    product.decrease();
                }
            });
            threads[i] = t;
            t.start();
        }

        for (Thread t : threads) {
            t.join();
        }

        System.out.println(product.getName() + "의 남은 재고 : " + product.getStock());
    }

 

상품에는 재고가 10,000개 있습니다.

 

동시에 100개의 스레드를 생성하고, 각 스레드별로 100번의 재고 감소 로직을 실행하게 됩니다.

 

100개의 스레드가 100번의 재고 감소를 수행하면, 재고는 0이 되어야 합니다.

 

실제 테스트 결과는 어떨까요??

 

테스트 결과
재고 출력 결과

 

테스트를 반복해서 실행해 보면, 재고가 0이 되어 예상했던 결과를 출력하기도 하지만 매번 결괏값이 달라집니다.

 

이러한 문제의 원인은 여러 스레드가 동시에 아래 로직을 실행하기 때문입니다.

 

stock = stock - 1;

 

해당 연산은 자바 코드상으로는 한 줄이지만, 실행될 때는 원자적으로 처리되지 않습니다.

 

다시 말해, 이 로직은 실제로 세 단계로 동작하게 됩니다.

 

  1. 메모리의 stock 값을 CPU로 읽어온다.
  2. CPU 내로 가져온 stock 값에서 1을 뺀다.
  3. 연산이 완료된 stock 값을 다시 메모리에 덮어쓴다.

한 줄의 자바 코드는 위의 세 줄로 구성되어 있고, 공유 자원인 stock에 접근하는 순서에 따라 결과가 달라집니다.

 

경쟁 상태

 

스레드 1,2는 모두 stock 값을 10,000으로 읽어오게 됩니다.

 

하지만, 스레드 1과 스레드 2는 모두 10,000으로 읽어온 값을 기준으로 stock 값을 변경하고 수정합니다.

 

따라서 9,998로 갱신되어야 할 값이 9,999로 잘못 갱신되는 결과가 발생합니다.

 

그렇다면 이러한 동시성 문제를 어떻게 해결할 수 있을까요?

 

이를 해결하기 위해서는, 코드 한 줄로 보이는 재고 감소 로직의 여러 실행 단계를 하나로 묶어, 한 번에 하나의 스레드만 안전하게 공유 자원에 접근할 수 있도록 제어해야 합니다.

 

 

이러한 동시성 문제는 프로세스 수준, DB 수준 모두에서 발생할 수 있으며, 위에서 다룬 예제는 프로세스 수준에서 발생하는 동시성 문제입니다.

 

Lock


잠금을 사용하면 공유 자원에 접근하는 스레드를 한 개로 제한할 수 있습니다.

 

잠금은 총 세 단계로 이루어집니다.

  1. 잠금 획득
  2. 공유 자원에 접근
  3. 잠금 해제

잠금은 한 번에 한 스레드만 획득할 수 있으므로, 여러 스레드가 동시에 잠금 획득을 시도하더라도 한 스레드만 공유 자원에 접근하고 나머지는 대기하게 됩니다.

 

잠금을 사용하는 동시성 제어 방식의 두 가지를 소개하겠습니다.

  • synchronized
  • Lock 인터페이스

 

synchronized


synchronized는 Java 환경에서 가장 간단하게 동시성을 제어할 수 있는 방법입니다.

 

synchronized 키워드는 두 가지 방법으로 설정할 수 있습니다.

 

 

첫 번째로 메서드 선언부에 키워드를 추가할 수 있습니다.

synchronized public void decrease() {
    this.stock = this.stock - 1;
}

 

synchronized 키워드가 붙은 메서드에는 한 번에 하나의 스레드만 접근 가능하며 해당 메서드 내부가 임계 영역이 됩니다.

 

 

두 번째로 코드 블럭 단위로 지정할 수 있습니다.

public void decrease() {
    synchronized (this) {
        this.stock = this.stock - 1;
    }
}

synchronized 블록을 사용하면 특정 객체에 대한 동기화를 수행할 수 있습니다.

 

이는 메소드 전체를 동기화하는 것보다 더 세밀한 제어가 가능합니다.

 

+

자바의 모든 객체는 내부에 모니터라는 고유한 락을 갖고 있습니다.

 

synchronized를 사용해 특정 객체의 모니터 락을 획득하여 동시성을 제어하게 됩니다.

 

따라서, synchronized(this)는 현재 객체에 대한 락을 의미합니다.

 

 

Lock 인터페이스


public class Product {
    private String name;
    private int stock;
    private Lock lock = new ReentrantLock();


    public void decrease() {
        lock.lock();
        try{
            this.stock = this.stock - 1;
        }finally {
            lock.unlock();
        }
    }

	...
}

 

Lock 인터페이스는 java.util.concurrent.locks 패키지에 포함되어 있습니다.

 

Lock 인터페이스를 사용하면 명시적으로 잠금을 획득하고 해제할 수 있으며, 시도-잠금(tryLock) 및 타임아웃 설정과 같은 보다 세밀한 제어가 가능합니다.

 

Lock 인터페이스를 사용할 때는 lock() 메서드로 락을 획득하여 임계 영역에 진입하고, 공유 자원 처리가 끝난 후에는 반드시 unlock()을 호출하여 락을 반환해야 합니다.

 

ReentrantLock 클래스를 확인해 보면, try-finally 구문을 활용하여 락 반환에 대한 안정성을 보장하는 방식을 추천하고 있습니다.

 

ReentrantLock은 synchronized에 없는 기능인 시도-잠금(try-lock) 및 타임아웃 설정을 제공합니다.

 

설정한 대기 시간 내에 락을 획득하지 못하는 경우, 무작정 대기하는 대신 즉시 실패를 반환할 수 있습니다.

 

따라서 스레드의 무한 대기로 인한 데드락을 예방할 수 있고, 사용자에게 즉각적인 피드백으로 사용자 경험을 확보할 수 있습니다.

 

 

+ ReentrantLock 내부 구현


해당 구현체의 내부 구현의 특징을 몇 가지 소개하겠습니다.

 

Re-entrant Lock (재진입 가능한 락)

이미 락을 획득한 스레드는 동일한 락을 중첩해서 획득할 수 있다는 의미로,

동일한 스레드가 같은 락을 사용하는 코드 블록이나 메서드에 재진입하더라도 블로킹되지 않습니다.

 

이미 락을 획득한 스레드가 lock()을 다시 호출하면 holdCount(재진입 횟수)를 증가시키며 관리합니다.

 

따라서, lock()을 호출한 횟수만큼 unlock()이 호출되어야만 락이 완전히 해제되고, 다른 스레드가 해당 락을 획득할 수 있습니다.

 

 

AQS (AbstractQueuedSynchronizer)

ReentrantLock은 자바 동시성 프레임워크인 AbstractQueuedSynchronizer를 기반으로 구현됩니다.

 

위에서 설명한 holdCount 변수를 원자적으로 관리하기 위해 AQS의 volatile int 변수인 state로 관리합니다.

 

이는 재진입 횟수 및 락의 획득 상태를 관리하기 위한 변수입니다.

 

 

또한, 락을 획득하지 못한 스레드들은 AQS 내부의 이중 연결 리스트 기반의 대기 큐에서 관리됩니다.

wait queue

 

Fair or Unfair

ReentrantLock은 생성자에 전달되는 boolean 인자에 따라 락 획득 전략을 선택할 수 있습니다.

 

 

Fair Lock은 큐에 대기 중인 스레드가 있으면 순서를 준수하여, 모든 스레드에게 동등한 기회를 제공합니다.

 

스레드는 동등한 기회를 얻을 수 있으므로, 스레드 기아 현상을 방지할 수 있습니다.

 

 

Unfair Lock은 Fair Lock보다 처리량이 높습니다.

 

하지만 Fair Lock과는 달리 순서를 준수하지 않으므로, 일부 스레드가 대기 큐에서 오랜 시간 대기할 가능성이 존재합니다.

 

 

 

참고


자바의 동기화 메커니즘 이해하기: synchronized와 Lock

Java ReentrantLock - 공정성, tryLock 등

주니어 백엔드 개발자가 반드시 알아야 할 실무 지식 - 최범균 저

반응형

'Teck Stack > Java' 카테고리의 다른 글

[Java] 동시성 제어 2탄 - 세마포어(Semaphore)  (0) 2025.12.28
[Java] 일급 컬렉션(First Class Collection)이란?  (0) 2025.10.25
[JAVA] Static은 언제 사용할까?  (0) 2025.10.24
[Java] Error와 Exception  (0) 2025.10.08
'Teck Stack/Java' 카테고리의 다른 글
  • [Java] 동시성 제어 2탄 - 세마포어(Semaphore)
  • [Java] 일급 컬렉션(First Class Collection)이란?
  • [JAVA] Static은 언제 사용할까?
  • [Java] Error와 Exception
taetae99
taetae99
우직하게 개발하기
    반응형
  • taetae99
    코드 대장간
    taetae99
  • 전체
    오늘
    어제
    • 분류 전체보기
      • Teck Stack
        • Java
        • Spring
        • DB
        • Redis
        • SpringSecurity
        • Docker
        • HTML
        • AWS
      • 우아한테크코스
      • CS & Architecture
        • DDD
        • CS
        • 디자인 패턴
      • 트러블 슈팅
      • 알고리즘
        • 프로그래머스
        • 백준
      • 프로젝트
        • Board 프로젝트
      • 기타
      • 대회 및 후기
  • 인기 글

  • hELLO· Designed By정상우.v4.10.3
taetae99
[Java] 동시성 제어 1탄 - Lock
상단으로

티스토리툴바