취뽀몽

[Java] 세마포어(Semaphore)와 뮤텍스(Mutex) 본문

java

[Java] 세마포어(Semaphore)와 뮤텍스(Mutex)

허몽구 2024. 11. 17. 17:42

최근에 프로젝트에서.. 자원에 대해 접근하는 사용자의 수를 제한하자는 얘기가 오고간 적이 있어서 공부하게 된 개념에 대해 정리하고자 한다.

 

자바에서 세마포어(Semaphore)와 뮤텍스(Mutex)는 멀티스레드 환경에서 공유 자원의 동기화와 경쟁 상태(race condition)를 방지하기 위한 동기화 도구이다. 이 두 개념은 공통적으로 스레드 간의 접근을 제어하지만, 동작 원리와 사용 사례에 차이가 있다.

 

우선, 경쟁 상태에 대해 알아보도록 하자.

 

1. 경쟁 상태(Race Condition)

경쟁 상태란 멀티스레드 환경에서 여러 스레드가 동시에 동일한 자원에 접근하거나 조작할 때 발생하는 문제를 말한다.
이로 인해 예상치 못한 동작이나 데이터 불일치, 예외적인 결과가 발생할 수 있다.

 

경쟁 상태의 원인은 다음과 같다.

 

1) 여러 스레드가 동시에 공유 자원에 접근하고, 해당 자원의 값을 읽거나 수정할 때, 작업 순서가 예측 불가능해짐.

2) 작업 간의 순서와 타이밍에 따라 결과가 달라질 수 있음.

3) 임계 구역(Critical Section)에서 적절한 동기화가 이루어지지 않을 때 발생함.

 

정리하자면 경쟁 상태는 멀티스레드 프로그래밍의 가장 흔한 문제 중 하나로, 공유 자원을 접근할 때 발생하게 되는 문제이다.

이 경쟁 상태를 방지하기 위해 세마포어(Semaphore)와 뮤텍스(Mutex)를 사용할 수 있다.

 

2. 뮤텍스(Mutex)

뮤텍스는 단일 스레드만 공유 자원에 접근하도록 보장하는 동기화 도구이다.
뮤텍스는 Mutual Exclusion(상호 배제)의 줄임말로, 특정 시점에 단 하나의 스레드만 임계 구역에 접근할 수 있게 만든다.

 

뮤텍스의 특징은 다음과 같다.

1) 단일 스레드 접근: 동시에 하나의 스레드만 공유 자원을 사용할 수 있다.

2) 소유권 : 뮤텍스를 획득한 스레드만 이를 해제할 수 있다.

3) 자바에서 뮤텍스는 ReentrantLock 또는 synchronized 키워드로 구현된다.

 

코드를 통해 사용 예시를 알아보도록 하자.

 

예시 - 1) ReentrantLock 사용

import java.util.concurrent.locks.ReentrantLock;

public class MutexExample {
    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(new Task(i));
            thread.start();
        }
    }

    static class Task implements Runnable {
        private final int threadNumber;

        Task(int threadNumber) {
            this.threadNumber = threadNumber;
        }

        @Override
        public void run() {
            lock.lock(); // lock 획득
            try {
                System.out.println("Thread " + threadNumber + " 작업 수행 중...");
                Thread.sleep(1000);
                System.out.println("Thread " + threadNumber + " 작업 완료.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
}

 

위 코드를 실행하면,

 

위와 같은 결과가 출력된다.

 

예시 - 2) synchronized 사용

public class MutexExample {
    public static void main(String[] args) {
        Resource resource = new Resource();

        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(() -> resource.access());
            thread.start();
        }
    }
}

class Resource {
    public synchronized void access() {
        System.out.println(Thread.currentThread().getName() + " 작업 수행 중...");
        
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println(Thread.currentThread().getName() + " 작업 완료...");
    }
}

 

 

두 개의 코드 모두 하나의 작업이 수행되고, 해당 작업이 완료되어야 다음 스레드 실행이 가능하다.

 

임계 영역에 이미 스레드가 접근 중일 경우, 다른 스레드가 공유 자원을 사용하려고 하면 다음과 같은 동작이 이루어진다.

 

1) 대기 큐에 추가 : 접근을 요청한 스레드를 대기 큐에 등록한다.

2) 스레드 차단(Block) : 현재 자원을 사용할 수 없으므로 해당 스레드를 Blocking 상태로 전환하여 실행을 중지한다.

3) 대기 상태(Sleep) : 자원이 해제되기를 기다리며 대기한다.

4) 자원 해제 시 실행 재개 : 임계 영역을 점유 중인 스레드가 자원을 해제하면 대기 큐에 있는 스레드 중 하나가 깨어나 자원을 사용한다.

 

이 과정을 통해 뮤텍스는 여러 스레드가 동시에 임계 영역에 접근하지 못하도록 제어하게 된다.

 

3. 세마포어(Semaphore)

세마포어는 동시에 허용할 수 있는 스레드의 개수를 제한하는 동기화 도구이다.

즉, 멀티 프로그래밍 환경에서 다수의 프로세스나 스레드의 자원에 대해 접근을 제한하는 방법이다.

세마포어는 내부적으로 허가(permission)라는 개념을 사용하여, 한정된 자원에 여러 스레드가 접근할 수 있도록 한다.

 

세마포어의 특징은 다음과 같다.

1) 동시성 관리 : 여러 스레드가 동시에 접근 가능하며, 최대 허용 스레드 수는 세마포어의 초기 허용 값으로 결정된다.

2) 공유 자원 접근 제어 : acquire() 메서드허가를 요청하며, 허가가 남아있지 않으면 스레드는 대기한다.

3) 허가 반환 : release() 메서드를 호출하면 허가를 반환하고, 대기 중인 다른 스레드가 실행될 수 있다.

 

세마포어도 예시를 통해 알아보도록 하자.

 

import java.util.concurrent.Semaphore;

public class SemaphoreExample {
    private static final Semaphore semaphore = new Semaphore(3); // 최대 3개 스레드 허용

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new Task(i));
            thread.start();
        }
    }

    static class Task implements Runnable {
        private final int threadNumber;

        Task(int threadNumber) {
            this.threadNumber = threadNumber;
        }

        @Override
        public void run() {
            try {
                semaphore.acquire(); // 허가 요청
                System.out.println("Thread " + threadNumber + " 작업 시작...");
                Thread.sleep(2000);
                System.out.println("Thread " + threadNumber + " 작업 완료...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                semaphore.release(); // 허가 반환
            }
        }
    }
}

 

위 코드를 실행하면 다음과 같이 결과가 출력된다.

 

3개의 스레드만 동시에 실행되며, 나머지 스레드는 대기 상태에 있다가 허가를 받을 수 있으면 실행된다.

 

세마포어의 실행 과정은 다음과 같다.

1) 초기 상태 : 스레드 1, 2, 3이 동시에 작업을 시작한다. (설정한 semaphore의 최대 허가 수가 3 이기 때문이다.)

2) 작업 진행 : 작업 시간이 2초(Thread.sleep(2000)) 로 설정되어 있으므로, 3개의 스레드가 2초 동안 작업을 수행한다.

3) 작업 완료 후 허가 반환 : 작업을 완료한 스레드는 semaphore.release()를 호출하여 허가를 반환한다. 이후 대기 중인 스레드 중 하나가 허가를 받아 작업을 시작하게 된다.

4) 반복 : 위 과정이 반복되면서 모든 스레드가 작업을 완료한다.

 

세마포어는 뮤텍스와 달리 여러 프로세스가 접근이 가능하다.

 


 

세마포어를 적용해볼 일이 있어서 정리해봤는데, 꽤나 재밌었다!

한 번도 써보지 않았던 개념을 적용시켜보니 흥미롭기도 한 것 같다. 뿌듯!

 

 

 

 

'java' 카테고리의 다른 글

[Java] SHA-256 암호화 알고리즘  (0) 2025.01.19
[Java] 메소드 체이닝  (0) 2024.12.14
[Java] 파라미터로 Optional을 전달하면?  (0) 2024.07.12
[Java] OOPS  (0) 2024.06.02
[Java] Synchronized  (0) 2024.05.22