멀티스레딩 환경에서 여러 스레드가 동시에 같은 리소스에 접근하려 할 때,
데이터의 일관성과 무결성을 유지하는 것이 중요합니다.
이를 위해 자바에서는 뮤텍스(Mutex)와 세마포어(Semaphore) 같은 동기화 메커니즘을 제공합니다.
이 글에서는 뮤텍스와 세마포어의 개념을 설명하고, 차이점을 자바 예제와 테스트코드로 함께 살펴보겠습니다.
뮤텍스(Mutex)
뮤텍스는 Mutual Exclusion(상호 배제)의 약자이다.
한 번에 하나의 스레드만이 특정 리소스나 코드 섹션에 접근할 수 있도록 합니다.
리소스에 접근하는 스레드가 뮤텍스를
1. '잠그고(lock)'
2. 작업을 한다
3. '해제(unlock)'
즉 한 시점에 단 하나의 스레드만이 리소스를 사용할 수 있게 됩니다.
자바에 ReentrantLock 라는 Lock 인터페이스의 구현체 클래스가 있습니다.
ReentrantLock 라는 녀석을 이용해서 락의 획득과 해제를 수기로 제어가 가능합니다.
자세한 사용법은 아래의 참고자료 오라클 홈페이지에서 읽어보세요.
뮤텍스(Mutex) 예제 및 테스트코드
// 예제코드
public class MutexExample {
private final Lock lock = new ReentrantLock();
public void accessResource(int threadId) {
// 자원 진입 시도
System.out.println("Thread " + threadId + " is trying to access the resource.");
lock.lock();
try {
// 자원 진입!
System.out.println("Thread " + threadId + " is accessing the resource.");
// 자원에 대한 작업을 수행하는 동안 지연을 추가하여 로그 출력 관찰
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 자원 사용 완료!
System.out.println("Thread " + threadId + " is releasing the resource.");
lock.unlock();
}
}
}
락으로 잡고 해제하고
진입시도, 진입, 완료를 로그로 찍는 코드입니다.
// 테스트코드
class MutexExampleTest {
@Test
@DisplayName("Mutex로 자원접근")
void mutexAccess() throws InterruptedException {
final MutexExample example = new MutexExample();
ExecutorService executor = Executors.newFixedThreadPool(5); // 동시에 5개의 스레드를 실행
for (int i = 0; i < 5; i++) {
final int threadId = i;
executor.submit(() -> example.accessResource(threadId));
}
executor.shutdown();
boolean finished = executor.awaitTermination(10, TimeUnit.SECONDS);
assert finished;
}
}
스레드 5개로 하나의 공유자원에 접근하는 뮤텍스 시나리오 테스트코드입니다.
실행 결과를 보면
하나의 스레드가 끝이나야 -> 다른 스레드가 진입을 할 수 있음을 알 수 있습니다.
세마포어(Semaphore)
세마포어는 리소스에 동시에 접근할 수 있는 스레드의 수를 제한합니다.
세마포어는 특정 수의 '허가증(permits)'을 가지고 있으며,
자바에서 semaphore클래스의 acquire() 메서드가 허가증을 주는 역할을 한다고 보시면 됩니다.
스레드가 리소스에 접근하기 위해서는 허가증을 획득해야 합니다.
모든 허가증이 사용 중일 때 추가 스레드는 허가증이 반환될 때까지 대기합니다.
자바예제로 바로 확인해보겠습니다
세마포어(Semaphore) 예제 및 테스트코드
// 예제코드
public class SemaphoreExample {
private final Semaphore semaphore = new Semaphore(3); // 동시에 3개의 스레드만 접근 가능
public void accessResource(int threadId) {
try {
// 자원 진입 시도
System.out.println("Thread " + threadId + " is trying to access the resource.");
// 허가증 획득 시도
semaphore.acquire();
// 자원 진입!
System.out.println("Thread " + threadId + " is accessing the resource.");
// 자원에 대한 작업을 수행하는 동안 일부 지연을 추가
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
System.out.println("Thread " + threadId + " is releasing the resource.");
semaphore.release();
}
}
}
// 테스트코드
class SemaphoreExampleTest {
@Test
@DisplayName("Semaphore로 자원접근")
void semaphoreAccess() throws InterruptedException {
final SemaphoreExample example = new SemaphoreExample();
ExecutorService executor = Executors.newFixedThreadPool(10); // 동시에 10개의 스레드를 실행
for (int i = 0; i < 10; i++) {
final int threadId = i;
executor.submit(() -> example.accessResource(threadId));
}
executor.shutdown();
boolean finished = executor.awaitTermination(20, TimeUnit.SECONDS);
assert finished;
}
}
스레드 10개로 3개의 공유자원(허가증)에 접근하는 세마포어 시나리오 테스트코드입니다.
3개의 스레드가 허용가능하고, 허가증 사용이 끝나면 진입이 가능한 것을 볼 수 있습니다.
뮤텍스와 세마포어 비교
기능 | 뮤텍스 | 세마포어 |
동시 접근 가능 스레드 수 | 1 | 제한된 수 (1 이상) |
사용 목적 | 상호 배제를 통한 단일 리소스 접근 제어 | 제한된 수의 리소스 동시 접근 제어 |
메커니즘 | Lock/Unlock | Acquire/Release |
결론
뮤텍스와 세마포어는 자바에서 동시성을 제어하는 데 필수적인 동기화 메커니즘입니다.
뮤텍스는 주로 상호 배제를 통해 리소스에 대한 독점적 접근을 제어하는 데 사용된다.
세마포어는 동시에 여러 스레드가 리소스에 접근할 수 있도록 제한적으로 허용하는 데 사용됩니다.
이러한 동기화 메커니즘을 통해 멀티스레딩 환경에서 데이터의 일관성과 무결성을 보장할 수 있습니다.
참고자료
https://docs.oracle.com/javase%2F8%2Fdocs%2Fapi%2F%2F/java/util/concurrent/locks/ReentrantLock.html
https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Semaphore.html