💋 인트로
지난 포스팅에 이어, 스레드 동기화 메커니즘 중 하나인 모니터에 대해 설명하겠습니다.
💋 모니터(Monitor)
✔️ 개념
baeldung의 모니터에 대한 글을 보면, 모니터를 아래와 같이 정의합니다.

모니터는 mutual exclusion과 cooperation을 통해 스레드 간의 안전한 공유자원 접근을 보장하는데 사용됩니다.
- mutual exclusion ⇒ 동시에 하나의 스레드만 진입해서 실행 가능
- cooperation ⇒ 조건에 따라 스레드가 대기 상태로 전환 가능
스레드가 어떤 공유자원에 접근하는 것을 말 그대로 모니터할 수 있기 때문에 모니터라고 부른다고 합니다.
✔️ 동작 원리
모니터는 뮤텍스와 condition variable로 구성되어 있으며, waiting queue와 entry queue를 통해 스레드들을 관리합니다.
condition variable과 관련된 주요 동작은 아래와 같습니다.
wait: 스레드가 자기 자신을 condition variable의 waiting queue에 넣고 대기 상태로 전환합니다.signal: waiting queue에서 대기중인 스레드 중 하나를 깨웁니다.broadcast: waiting queue에서 대기중인 스레드 전부를 깨웁니다.
코드로 이해해 봅시다!
acquire(mutexLock); // 뮤텍스 락 취득 -> 락 취득 못하면 entry queue에서 대기
while (!p) { // 조건 확인 -> 조건이 충족되지 않으면 waiting queue에서 대기
wait(mutexLock, conditionVariable);
}
// ...
signal(conditionVariable2); // 앞의 conditionVariable과 다를 수도 있음.
// broadcast(conditionVariable2);
release(mutexLock); // 뮤텍스 락 반환
위 코드는 대충의 뼈대이고, 각각을 실행하는 과정을 풀어서 설명하면 아래와 같습니다.
acquire(mutexLock);- 뮤텍스 락을 취득하지 못한 스레드는 entry queue에서 대기하게 됩니다.
- 조건 확인
mutexLock이 파라미터인 이유 ⇒ 대기 상태로 전환된 스레드가 뮤텍스 락을 가지고 있으면 다른 스레드들이 뮤텍스 락을 취득할 수 없기 때문에,wait를 호출과 함께 락을 반환하기 위해서입니다.conditionVariable이 파라미터인 이유 ⇒conditionVariable별로 waiting queue를 가지고 있기 때문입니다.
- 조건이 충족되지 않은 스레드는
wait(mutexLock, conditionVariable);호출해 waiting queue에서 대기하게 됩니다. - 조건까지 확인된 스레드는 자신의 할 일을 수행하는데, 이 코드는 위에서 …입니다.
signal(conditionVariable2);- 어떤 waiting queue의 스레드를 깨울지 그 condition variable을 통해서 관리할 수 있습니다.
conditionVariable2은 앞서 등장한conditionVariable와 반드시 같을 필요는 없습니다.broadcast를 사용하는 경우에는conditionVariable2를 통해 관리하는 waiting queue에 있는 스레드 모두를 깨울 수 있습니다.
conditionVariable2를 통해 관리하는 waiting queue에 있는 스레드 중 하나를 깨웁니다.
이때 등장하는 두 가지 Queue를 정리해보면 다음과 같습니다.
✔️ Queue
- entry queue: 뮤텍스 락을 취득하기 위해 기다리는 큐
- waiting queue: 조건이 충족되길 기다리는 큐
💋 Bounded buffer problem
(Producer-Consumer Problem이라고도 합니다.)
하나의 문제 예시를 설명하고, 앞서서 열심히 공부한 모니터를 사용해서 이 문제를 어떻게 해결할 수 있는지 보여드리겠습니다.
✔️ 구성

- Producer: 데이터를 생성하는 역할
- Consumer: 데이터를 소비하는 역할
- Buffer: 데이터가 저장되고, 공유되는 공간인데, limited capacity
✔️ 목표
- Producer는 버퍼가 꽉 찼을 때 더이상 데이터를 생산하지 않습니다.
- Consumer는 이미 소비한 데이터를 또 소비하지 않습니다.
- Buffer는 제한된 capacity를 효율적으로 사용합니다.
모니터를 사용해서 이 목표를 이룰 수 있습니다.

Producer, Consumer는 Buffer를 공유해서 사용하기 때문에, Buffer에 접근할 때는 critical section 안에서, mutual exclusion이 보장된 상태여야만 합니다.
Producer, Consumer 코드에서 공통적으로 lock.acquire(), lock.release() 사이의 부분은 critical section이 됩니다.
Producer의 스레드는 Buffer가 다 찼다면(q.isFull()), wait를 하게 됩니다. Consumer 스레드가 데이터를 소비한 후에 signal()을 호출해 Producer의 스레드가 깨어날 수 있게 됩니다. Producer 스레드는 데이터를 생산한 후에 signal()을 호출해서 Consumer의 스레드를 깨울 수 있습니다.
Producer 스레드는 자신의 작업을 완료한 후 Consumer 스레드를 깨우고,
Consumer 스레드는 자신의 작업을 완료한 후 Producer 스레드를 깨웁니다. (크로스!!!)
💋 Monitor in Java
자바의 모든 객체는 내부적으로 모니터를 가집니다.
자바의 모니터의 mutual exclusion 기능은 synchronized 키워드로 사용할 수 있으며, condition variable은 딱 하나만 가집니다.
✔️ 모니터 동작
자바 모니터는 세 가지 동작이 가능합니다.
- wait
- notify →
signal()과 동일 - notifyAll →
broadcast()과 동일
✔️ 자바로 구현한 Bounded buffer problem
public class BoundedBuffer {
private final int[] buffer = new int[5];
private int count = 0;
public synchronized void produce(int item) {
while (count == 5) {
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
buffer[count++] = item;
notifyAll();
}
public void consume() {
int item = 0;
synchronized (this) {
while (count == 0) {
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
item = buffer[--count];
notifyAll();
}
}
}
produce(), consume()의 두 메서드가 동시에 실행되지 않도록 하기 위해 synchroized 키워드를 통해 mutual exclusion을 보장합니다.
synchroized 키워드는 메서드에 붙일 수도 있고, 메서드 내부의 block에서만 사용할 수도 있습니다.
synchroized 을 block으로 사용하는 경우에 파라미터를 전달해 주어야 하는데, 이 파라미터를 통해 뮤텍스 락을 전달해주어야 합니다.
this는 객체 자신을 가리는데, 이걸 통해 뮤텍스 락을 전달할 수 있을까요?
자바의 모든 객체는 모니터를 가지고 있고 그 모니터에는 뮤텍스 락이 있기 때문에 this를 전달하는 것은 뮤텍스 락을 전달한다는 의미와 동일합니다.
wait(), notifyAll()은 자신의 객체가 속한 모니터의 condition variable에 대한 동작입니다. wait()과 notifyAll()은 synchronized 메서드 또는 블록 내에서 호출되어야 합니다.
위의 예제에서는 여러 개의 condition variable이 사용되었기 때문에, 각 조건에 따라 다른 condition variable을 파라미터로 함께 사용했는데, 자바에서는 딱 1개의 condition variable만을 가지기 때문에 별도로 condition variable을 파라미터로 보내지는 않습니다.
public class Main {
public static void main(String[] args) throws InterruptedException {
final BoundedBuffer boundedBuffer = new BoundedBuffer();
final Thread consumer = new Thread(() -> boundedBuffer.consume());
final Thread producer = new Thread(() -> boundedBuffer.produce(100));
consumer.start();
producer.start();
consumer.join();
producer.join();
System.out.println("Bounded Buffer Test Done.");
}
}
여러 번 시도하더라도, 항상 Producer가 데이터를 생산한 후에야 Consumer가 데이터를 소비하기 때문에, 어쨌든 Consumer가 100이라는 데이터를 소비한 후에 종료되는 것을 확인할 수 있습니다.

이외에도 java.util.concurrent에는 동기화 기능이 탑재된 여러 클래스가 있으니, 안심하고 사용할 수 있습니다.
💋 아웃트로
오늘 포스팅을 작성하면서 나름대로의 언어로 정리한 후에 GPT 선생님의 검토를 받았는데, 극찬을 받았어요ㅋㅋ

💋 참고자료
- https://www.baeldung.com/cs/monitor
- https://www.baeldung.com/cs/bounded-buffer-problem
- https://binaryterms.com/producer-consumer-problem.html
- https://stackoverflow.com/questions/3362303/whats-a-monitor-in-java
- https://www.youtube.com/watch?v=Dms1oBmRAlo&list=PLcXyemr8ZeoQOtSUjwaer0VMJSMfa-9G-&index=7

도움이 되었다면, 공감/댓글을 달아주면 깃짱에게 큰 힘이 됩니다!🌟
비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!
'컴퓨터과학 > 운영체제' 카테고리의 다른 글
| [OS] OS, Java에서 프로세스의 상태 (State of Process in OS and Java) (0) | 2023.11.23 |
|---|---|
| [OS] 데드락(Deadlock)은 언제 발생하고, OS, Java에서 어떻게 해결할까? (1) | 2023.11.22 |
| [OS] 동기화 메커니즘(Synchronization Mechanisms)(1): 스핀락(Spinlock), 뮤텍스(Mutex), 세마포어(Semaphore) (0) | 2023.11.20 |
| [OS] 동기화(synchronization)의 필요성: 경쟁 조건(race condition), 임계 영역(critical section) (0) | 2023.11.12 |
| [OS] CPU Bound VS IO Bound: 스레드는 몇 개가 좋을까? (0) | 2023.11.10 |