자바의 여러 클래스에서 Synchronized 예약어를 통한 동기화된 블록 혹은 메서드를 심심찮게 볼 수 있다.
당장 생각나는 클래스를 나열하면 StringBuffer
, HashTable
, Vector
, ConcurrentHashMap
등이 있다. 아래 java.util.concurrent
패키지에 많으니 참고하길 바란다.
자바에서의 monitor
와 synchronized
동작 방식에 대해 학습하고 정리해보려 한다.
그림 그려놨는데 파일이 날아가서 글로만 떼운다.
Java Monitor
monitor
라는 개념이 존재한다.
monitor는 여러 스레드가 동시에 객체로 접근하는 것을 막을 수 있고(mutual exclusion 기능을 제공), 이와 관련된 기능을 제공한다.
A monitor consists of a mutex (lock) and at least one condition variable - wikipidia
monitor
는 내부적으로 mutex
(lock)와 하나 이상의 condition variable
로 이루어져있다.
condition variable
은 특정 조건(ciritical section에서의 작업을 수행하기 위한 조건, 진입하기 위한 조건 등등,, )을 대기하는 스레드가 저장되는 공간인 wating queue
를 가진다.
mutex
에는 lock
을 포함하여 critical section에 진입하길 대기중인 스레드가 저장되는 공간인 entry queue
를 가진다.
정리하면
- 모니터에는 mutex와 condition variable(s)이 존재한다.
- mutex는 우리가 흔히 생각하는 lock이다.
- mutex에는 스레드가 락을 획득하길 대기하는 entry queue가 존재한다.
- condition variables는 스레드들의 통신과 조정에 사용되는 매커니즘이다.
- condition variable이 제공하는 wait를 통해 스레드는 waiting queue에서 notify가 호출되기 까지 대기한다.
모니터가 mutual explosion기능을 수행하는 방식을 간단하게 정리하면 아래와 같다.
- A 스레드가 크리티컬 섹션에 진입하며
mutex lock
(이하 락) 을 획득한다. - B 스레드가 크리티컬 섹션에 진입하기 위해
entry queue
에서 락 획득을 기다린다. - A 스레드가 작업을 수행하려고 보니 작업 조건을 불충족하여 락을 반납하고
waiting queue
에서 작업 조건 충족을 기다린다. - B 스레드가 락을 획득하고 크리티컬 섹션에 진입하여 작업을 수행한다. 작업을 끝마치면 waiting queue에서 대기중인 스레드를 깨우고, 락을 반납한다.
- A 스레드가 락을 획득하고 작업을 수행한다.
참조 영상에 자세하게 설명되어 있으니 참고하시길 바란다.
자바에서는 어떨까?
Every Java object is associated with a Java monitor and is implemented using the synchronized keyword. A monitor ensures that only one thread can execute a synchronized block of code or method on an object at a time. When a thread enters a synchronized block, it acquires the monitor associated with the object, and other threads attempting to enter the same or different block will be blocked until the first thread releases the monitor.
자바의 모든 Object
는 내부적으로 monitor
를 가지고 있다.
자바의 monitor
는 condition variable
을 하나만 가진다.
monitor
에서 제공하는 condition variable
과 연관된 메서드(동작)은 3가지이며, wait()
, notify()
, notifyAll()
이다.
스레드가 모니터에 진입했을 때, wait()
메서드를 통해 waiting queue
로 들어갈 수 있다.
notify()
, notifyAll()
메서드를 통해 waiting queue의 스레드를 깨울 수 있다.
그래서 synchronized는 어떻게 동작하는가?
코드를 보니 Synchronized
블럭에 Object
를 넣어줘서 어떻게 동작하던데 이게 너무 궁금했다.
// Only one thread can execute at a time.
// sync_object is a reference to an object
// whose lock associates with the monitor.
// The code is said to be synchronized on
// the monitor object
synchronized(sync_object)
{
// Access shared variables and other
// shared resources
}
synchronized(Object) 안에 들어가는 Object가 모니터의 뮤텍스가 되는 것이라고 한다.
한 스레드가 synchronized
블럭 혹은 메서드에 진입하여 lock
을 획득한다.
다른 스레드가 synchronized
블럭 혹은 메서드에 진입하려고 한다면 이미 lock
은 다른 스레드에게 있기 때문에 동기화 블럭 혹은 메서드에 진입하지 못하고 큐
에서 대기하게 된다.
대기하는 스레드는 lock
이 반납될 때까지 대기하며 이때, 스레드의 상태를 Thread.State.BLOCKED
라고 한다.
만약, 여기서 락을 획득한 스레드가 락을 계속 반납하지 않으면서 작업을 수행하고, 대기하는 스레드가 무한히 락을 기다리게 되면서 프로세스가 멈추는 현상을 데드락이라고 한다.
자바 코드로 구현해보자.
멀티 스레드 환경에서 버퍼자원을 공유하며 아이템의 공급과 소비를 수행하는 코드를 간단하게 짜보았다.
public class BoundedBuffer {
private final String[] buffer;
private final int bufferSize;
private int count;
public BoundedBuffer(String[] buffer) {
this.buffer = buffer;
bufferSize = buffer.length;
}
// 버퍼에 아이템 공급
public synchronized void supply(String item) {
while (count == bufferSize) { // 버퍼가 가득 찼다면 waiting queue에 대기
try {
this.wait();
} catch (InterruptedException ignored) { }
}
buffer[count++] = item;
this.notifyAll(); // waiting queue에 존재하는 스레드 모두 깨우기
}
// 버퍼에 존재하는 아이템 소비
public String consume() {
String item;
synchronized (this) {
while (count == 0) { // 버퍼가 비었다면 waiting queue에 대기
try {
this.wait();
} catch (InterruptedException ignored) { }
}
item = buffer[--count];
this.notifyAll(); // waiting queue에 존재하는 스레드 모두 깨우기
}
return item;
}
}
import java.util.LinkedList;
import java.util.Queue;
public class Main {
public static void main(String[] args) {
BoundedBuffer buffer = new BoundedBuffer(new String[5]); // 5개짜리 버퍼 생성
Queue<String> itemBox = new LinkedList<>();
for (int i = 0; i < 6; i++) {
itemBox.add("item" + i);
}
int consumerCount = 0;
int supplierCount = 0;
while (consumerCount < 6 && supplierCount < 6) {
// 버퍼의 아이템을 소비하는 스레드
Thread consumer = new Thread(() -> {
String item = buffer.consume();
System.out.println(Thread.currentThread().getName() + " consume : " + item);
}, "consumer" + consumerCount++); // name
// 버퍼의 아이템을 공급하는 스레드
Thread supplier = new Thread(() -> {
if (itemBox.isEmpty()) return;
String item = itemBox.poll();
System.out.println(Thread.currentThread().getName() + " supply : " + item);
buffer.supply(item);
}, "supplier" + supplierCount++); // name
consumer.start();
supplier.start();
}
}
}
코드를 짤 때 주의해야할 점이 있다.
while (count == bufferSize) {
try {
this.wait();
} catch (InterruptedException ignored) { }
}
wait는 꼭 while문 안에서 걸어줘야 한다는 것이다.
waiting queue에서 대기중인 스레드가 notify되어 다시 뮤텍스 락을 획득하여 임계영역에서 동작할 때,
wait 메서드 이후부터 시작하기 때문이다.
만약 위의 코드에서 while문이 if문이라고 생각해보자
스레드가 임계영역에 접근하고 보니 buffer가 가득 차서 wait를 호출하고 waiting queue에서 대기한다.
다시 깨어나서 뮤텍스 락을 획득하고 실행하려고 보면 이미 if문은 빠져나온 상태인 것이다.
여기서 buffer가 가득 찬 상태임에도 다시 wait가 걸리지 않고 로직을 수행해버려 오류가 발생할 수 있다.
질문
- synchronized가 무엇인가?
더보기자바의 멀티스레드 환경에서 동기화를 위해 mutual exclusion기능을 사용할 수 있게 해주는 예약어입니다.
- synchronized는 어떻게 동작하는가?
더보기monitor라는 개념이 있는데, 모든 자바 객체에는 이 모니터라는 것이 있습니다.모니터에는 스레드의 상태를 관리하는 condition variable 과 critical section으로의 동시 접근을 제어하는 mutex가 존재합니다.스레드가 mutex lock을 얻고 critial section으로 진입하면 다른 스레드는 mutex 내부에 있는 entry queue에서 대기하게 됩니다만약 작업을 수행하던 스레드가 작업 조건을 만족하지 못하여 wait메서드를 호출하면 condition variable에 존재하는 waiting queue에서 대기하게 됩니다.다음 스레드드가 mutex lock을 얻고 critical section에서 작업을 수행한 뒤 notify 메서드를 호출하게 되면 wating queue에서 대기중인 스레드가 entry queue 혹은 mutex lock을 얻고 wait가 호출되었던 장소에서 부터 작업을 수행하게 됩니다.
- monitor란 무엇인가?
더보기멀티스레드 환경에서 동기화를 위해 mutual exclution기능을 사용할 수 있게 해주는 개념입니다.내부적으로 mutex와 하나 이상의 condition variable을 가지고 있고 이를 통해 mutual exclusion 기능을 제공합니다.
- mutex란 무엇인가?
더보기mutual exclusion의 약어 입니다. 멀티 스레드 환경에서 critical section으로의 동시 접근을 제어합니다.
reference
재밌는 글들
'의문과 실험' 카테고리의 다른 글
제네릭은 왜 Lower Bounded를 지원하지 않을까? 와일드카드의 사용처는? (1) | 2024.01.09 |
---|---|
메인 메서드에 대한 고찰 (0) | 2024.01.03 |
상속 시, 오버라이딩된 메서드의 접근제어자는 왜 확장만을 허용할까 (0) | 2023.12.19 |
[Spring] 프로젝트에서 IO를 줄여 성능을 개선해보자 (0) | 2023.10.04 |
[Spring] JPA 엔티티에는 접근자 메서드외의 다른 메서드가 선언되어도 되는가 (0) | 2023.08.15 |