CodeGym /행동 /JAVA 25 SELF /뮤텍스와 세마포어: 문법과 과제

뮤텍스와 세마포어: 문법과 과제

JAVA 25 SELF
레벨 52 , 레슨 3
사용 가능

1. 뮤텍스(Mutex): 무엇이며 어떻게 동작하는가

뮤텍스(영어 “mutual exclusion” — “상호 배제”)는 한 번에 오직 하나의 스레드만 임계 구역 코드를 실행하도록 허용하는 메커니즘입니다. 뮤텍스가 점유(다른 스레드가 획득)된 상태라면, 나머지 스레드는 해제될 때까지 대기합니다.

Java에서는 코드가 동기화되는 대상 객체 자체가 자주 뮤텍스 역할을 합니다: synchronized. Java 5 버전부터는 ReentrantLock 클래스가 도입되어, 더 명시적이고 유연한 뮤텍스 구현을 제공합니다.

도식적으로

열쇠가 하나뿐인 방을 상상해 보세요(그 열쇠가 뮤텍스). 들어가려면 열쇠를 가져야 합니다. 열쇠가 없으면(누군가가 이미 가져갔다면) 문 앞에서 기다립니다. 열쇠가 제자리에 돌아오면(뮤텍스가 해제되면) 다음 사람이 들어갈 수 있습니다.

Java에서 뮤텍스 문법

synchronized 사용(고전적인 방식):

public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }
}

여기서는 increment 메서드 전체가 뮤텍스로 보호됩니다 — 현재 시점에 오직 하나의 스레드만 실행할 수 있습니다.

ReentrantLock 사용(더 유연):

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Counter {
    private int count = 0;
    private final Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock(); // 뮤텍스 획득
        try {
            count++;
        } finally {
            lock.unlock(); // 반드시 해제!
        }
    }
}

중요! 항상 finally 블록에서 뮤텍스를 해제하세요. 그렇지 않으면 “영원한 블로킹”(deadlock)을 일으켜 프로그램이 멈출 수 있습니다.

언제 뮤텍스가 필요한가?

한 번에 하나의 스레드만 자원에 접근해야 할 때 뮤텍스가 필요합니다. 자원은 변수, 파일, 데이터베이스가 될 수 있습니다. 작업이 원자적이지 않다면 특히 중요합니다. 예를 들어 단순한 count++ 역시 실제로는 값 읽기, 증가, 다시 쓰기의 세 단계로 구성됩니다. 뮤텍스 없이 여러 스레드가 이 단계 사이에 끼어들면 경합(race) 상태가 발생할 수 있습니다.

2. 세마포어(Semaphore): 왜 필요하며 어떻게 동작하는가

세마포어는 동시에 자원에 접근할 수 있는 스레드 수를 지정된 한도까지만 허용하는 “조절기”입니다. 한도가 다 찼다면, 나머지 스레드는 자신의 차례를 기다립니다.

비유: 3대 주차 공간이 있는 주차장. 모든 자리가 찼다면, 다른 차들은 누군가가 빠져나갈 때까지 대기합니다.

Java에서 세마포어 문법

이를 위해 java.util.concurrent 패키지의 Semaphore 클래스를 사용합니다:

import java.util.concurrent.Semaphore;

public class ParkingLot {
    private final Semaphore spots;

    public ParkingLot(int places) {
        this.spots = new Semaphore(places);
    }

    public void parkCar(String car) throws InterruptedException {
        spots.acquire(); // 자리를 차지 시도(없으면 대기)
        try {
            System.out.println(car + " 가 주차했습니다.");
            Thread.sleep(1000); // 자동차가 주차장에 머무는 중
        } finally {
            spots.release(); // 자리 해제
            System.out.println(car + " 가 떠났습니다.");
        }
    }
}

사용:

ParkingLot parking = new ParkingLot(3);

for (int i = 1; i <= 5; i++) {
    final String car = "자동차 " + i;
    new Thread(() -> {
        try {
            parking.parkCar(car);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();
}

결과: 동시에 주차장에 3대 이상 주차되지 않으며 — 나머지는 대기합니다.

세마포어는 어떻게 동작하나?

  • 세마포어를 생성할 때 “허용량”(permits) 개수를 지정합니다.
  • acquire()는 허용량을 가져오려 시도합니다. 여유가 있으면 통과하고, 없으면 기다립니다.
  • release()는 허용량을 반환합니다.
  • 허용량이 1인 세마포어는 거의 뮤텍스처럼 동작하지만 ‘소유자’ 개념이 없습니다.

3. 뮤텍스와 세마포어: 차이점은?

특성 뮤텍스 (Mutex) 세마포어 (Semaphore)
스레드 수 오직 1개 여러 개(제한된 수)
용도 자원 보호 접근 제한(예: 풀)
Java의 API
synchronized, Lock
Semaphore
관리 일반적으로 ‘소유자’가 있음 어떤 스레드나 해제 가능
대표 시나리오 공유 카운터, 객체 연결 풀, 주차장, 한도

- 뮤텍스 — 배타적 접근이 필요할 때.
- 세마포어 — 여러 개는 허용하지만 모두는 아닐 때.

비유: 뮤텍스 — 칸 하나짜리 화장실; 세마포어 — 칸이 세 개인 화장실.

4. 실전 예제

예제 1: 임계 구역 보호를 위한 뮤텍스

공유 은행 계좌가 있고, 여러 스레드가 계좌 간 송금을 한다고 가정해 봅시다. 연산은 원자적으로 이뤄져야 합니다.

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class BankAccount {
    private int balance;
    private final Lock lock = new ReentrantLock();

    public BankAccount(int initial) {
        this.balance = initial;
    }

    public void deposit(int amount) {
        lock.lock();
        try {
            balance += amount;
        } finally {
            lock.unlock();
        }
    }

    public void withdraw(int amount) {
        lock.lock();
        try {
            if (balance >= amount) {
                balance -= amount;
            }
        } finally {
            lock.unlock();
        }
    }

    public int getBalance() {
        return balance;
    }
}

여기서는 잔액에 대한 모든 연산이 뮤텍스로 보호되어 race condition이 발생하지 않습니다.

예제 2: 접근 제한을 위한 세마포어

서버는 동시에 2명의 클라이언트만 처리할 수 있습니다(예: 라이선스 제한).

import java.util.concurrent.Semaphore;

public class Server {
    private final Semaphore connections = new Semaphore(2);

    public void handleRequest(String client) throws InterruptedException {
        connections.acquire();
        try {
            System.out.println(client + " 가 서버에 연결되었습니다.");
            Thread.sleep(2000); // 요청 처리 시뮬레이션
        } finally {
            connections.release();
            System.out.println(client + " 의 연결이 종료되었습니다.");
        }
    }
}

사용:

Server server = new Server();

for (int i = 1; i <= 5; i++) {
    final String client = "클라이언트 " + i;
    new Thread(() -> {
        try {
            server.handleRequest(client);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();
}

결과: 동시에 서버는 두 명을 초과하여 클라이언트를 처리하지 않습니다.

5. 사용 시 주의사항과 팁

뮤텍스: 반드시 해제하라!
예외가 발생하더라도 unlock()(또는 동기화 블록을 벗어남)을 호출하는 것을 잊지 않는 것이 매우 중요합니다. try-finally를 사용하세요:

lock.lock();
try {
    // 임계 구역
} finally {
    lock.unlock();
}

잊어버리면 “영원한 블로킹”이 발생할 수 있으며, 다른 스레드는 무한히 대기하게 됩니다.

세마포어: 다른 스레드가 해제해도 될까?
뮤텍스와 달리 release()acquire()를 호출하지 않은 스레드라도 호출할 수 있습니다. 때로는 유용하지만, 실수하기 쉽습니다 — 규칙을 지키세요.

허용량 1인 Semaphore = 뮤텍스?
거의 그렇습니다. 하지만 세마포어에는 ‘소유자’ 개념이 없습니다. 어떤 해제든 허용량 카운터를 증가시킵니다. 반면 뮤텍스는 획득한 스레드가 해제해야 합니다.

세마포어와 풀을 혼동하지 말 것
세마포어는 객체 풀 자체가 아니라 단지 “허용량 카운터”일 뿐입니다. 세마포어로 풀(예: DB 연결 풀)을 구현하는 경우가 많지만, 세마포어 자체는 아무 것도 저장하지 않습니다.

6. 뮤텍스/세마포어 사용 시 흔한 오류

오류 №1: unlock/release 호출 누락. 뮤텍스나 세마포어를 획득하고 unlock() 또는 release()를 호출하지 않으면, 다른 스레드가 영원히 대기할 수 있습니다. 항상 try-finally를 사용해 예외가 있어도 잠금이 해제되도록 하세요.

오류 №2: 잘못된 객체에 동기화. 모든 스레드에 공통이 아닌 변수(예: 지역 변수나 문자열 리터럴)에 대해 동기화하면, 동기화가 제대로 동작하지 않습니다.

오류 №3: 이중 해제. 세마포어의 경우 acquire()보다 release()를 더 많이 호출하면 허용량이 한도를 넘겨 증가합니다. 균형을 맞추세요!

오류 №4: 뮤텍스 대신 세마포어 사용(또는 그 반대). 배타적 접근이 필요하면 뮤텍스(synchronized 또는 Lock)를 사용하세요. 동시에 동작할 수 있는 스레드 수를 제한하려면 Semaphore를 사용하세요.

오류 №5: 잠금을 오래 잡고 있음. 스레드가 뮤텍스나 세마포어를 오래 보유할수록 다른 스레드는 더 오래 기다립니다. 임계 구역 내 실행 시간을 최소화하세요.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION