Monitor
Java의 모든 객체는 하나의 락과 연결되어 있다. 메서드가 synchronized로 선언된 경우 메서드를 호출하려면 그 객체와 연결된 락을 획득해야 한다.
다른 스레드가 이미 락을 소유한 경우 synchronized 메서드를 호출한 스레드는 봉쇄되어 객체의 락에 설정된 진입 집합(entry set)에 추가된다.
- 진입 집합은 락이 가용해지기를 기다리는 스레드의 집합을 나타낸다
락을 획득한 스레드가 (synchronized)메서드를 종료하면 락이 해제된다.
락이 해제될 때 락에 대한 진입 집합이 비어있지 않으면 JVM은 이 집합에서 락 소유자가 될 스레드를 임의로 선택한다.
- JVM 명세에서 정의하지 않는다. == 구현따라 다르다.

락 외에도 모든 객체는 스레드 지합으로 구성된 대기 집합과 연결된다.
대기 집합은 초기에는 비어있다. 스레드가 synchronized 메서드에 들어가면 객체 락을 소유한다. 그러나 . 이스레드는 특정 조건이 충족되지 않아 계속할 수 없다고 결정할 수 있다.
- 예를들어, 생산자가 insert()를 호출했는데 버퍼가 가득 찬 경우.
그러면 스레드는 락을 해제하고 조건이 충족될 때까지 기다린다.
wait()
스레드가 wait()을 호출하면
- 스레드가 객체의 락을 해제한다
- 스레드 상태가 봉쇄됨으로 설정된다
- 스레드는 . 그객체의 대기 집합에 넣어진다
유명한 생산자-소비자 문제를 살펴보자
/* Producers c a ll th is method */
public synchronized void in sert (E item) {
while (count == BUFFER_SIZE) {
try {
wait();
} catch (InterruptedException ie ) { }
}
buffer[in] = item ;
in = (in + 1) % BUFFER_SIZE;
count++;
notify();
}
/* Consumers call this method */
public synchronized E remove () {
E item;
while (count == 0) {
try {
wait();
} catch (InterruptedException ie) { }
}
item = buffer[out];
out = (out + 1) % BUFFER.SIZE;
count-- ;
notify();
return item;
}
- 생산자가 isnert()를 호출하고 버퍼가 가득 찬 것을 확인하면 wiat()을 호출한다.
- 이 호출로 락은 해제되고 생산자를 봉쇄하여 객체의 대기 집합에 넣는다.
- 락이 해제됐기 때문에 소비자는 remove() 메서드로 진입하여 버퍼의 공간을 비운다.
그러면 소비자 스레드는 생산자가 이제 진행할 수 있다는 것을 어떻게 알릴까?
보통 스레드가 synchronized 메서드를 종료하면 이탈 스레드는 객체와 연결된 락만 해제하여 진입 집합에서 스레드를 제거하고 락 소유권을 넘겨준다. 그러나 위 insert() / remove() 메서드의 끝에서 우리는 notify() 메서드를 호출한다.
notify()
notify() 메서드는 다음과 같은 일을 한다.
- 대기 집합의 스레드 리스트에서 임의의 스레드 T를 선택한다.
- 스레드 T를 대기 집합에서 진입 집합으로 이동한다
- T의 상태를 봉쇄됨에서 실행 가능으로 설정한다
이로써 T는 이제 다른 스레드와 락 경쟁을 할 수 있다. (그냥 주는게 아니다!)
T가 락 제어를 다시 획득하면 wait() 호출에서 복귀하여, 실행흐름을 이어가며 count 값을 다시 확인할 수 있다.
Re-entrant Lock
https://jenkov.com/tutorials/java-concurrency/locks.html
재진입 가능한 락
API로 제공하는 가장 간단한 락
ReentrantLock은 synchronized와 유사하게 동작. 단일 스레드가 소유하며 공유 자원에 대한 상호 배타적 액세스를 제공한다
+공정성 매개변수 설정과 같은 몇 가지 추가 기능을 제공한다
- 공정성: 오래 기다린 스레드에 락을 줌
재진입 가능?
⇒ 이미 락을 획득한 스레드가 같은 락을 다시 획득할 수 있다!
왜 필요하냐?
https://stackoverflow.com/questions/1312259/what-is-the-re-entrant-lock-and-concept-in-general
메서드 A가 락을 획득한 상태에서 메서드 B를 호출
메서드 B도 같은 락을 필요로 할 때 데드락 없이 진행가능!
- 예시
- public class ReentrantExample { private final ReentrantLock lock = new ReentrantLock(); public void outer() { lock.lock(); // 첫 번째 락 획득 try { System.out.println("Outer method"); inner(); // inner 메서드 호출 } finally { lock.unlock(); } } public void inner() { lock.lock(); // 같은 스레드가 다시 락 획득 가능 try { System.out.println("Inner method"); } finally { lock.unlock(); } } }
ReentrantLock의 특징
- 명시적인 락킹
ReentrantLock lock = new ReentrantLock();
lock.lock(); // 명시적으로 락을 획득
try {
// 임계 영역 코드
} finally {
lock.unlock(); // 반드시 unlock 호출
}
- 공정성 옵션을 제공한다
⇒ 생성자에 true를 전달하면 가장 오래 기다린 스레드가 우선적으로 락을 획득하게 된다.
// 공정한 락 생성
ReentrantLock fairLock = new ReentrantLock(true);
- 다양한 락 획득 시도 방법
// 인터럽트 가능한 락 획득
try {
lock.lockInterruptibly();
// 작업 수행
} catch (InterruptedException e) {
// 인터럽트 처리
}
// 논블로킹 락 획득 시도
if (lock.tryLock()) {
try {
// 작업 수행
} finally {
lock.unlock();
}
}
// 타임아웃이 있는 락 획득 시도
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
// 작업 수행
} finally {
lock.unlock();
}
}
- Condition 지원
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
// 생산자 스레드
lock.lock();
try {
while (버퍼가_가득참) {
condition.await(); // 대기
}
// 데이터 추가
condition.signal(); // 다른 스레드에게 신호
} finally {
lock.unlock();
}
https://ttl-blog.tistory.com/799
- Condition은 왜 필요할고, await()는 무엇을 할까?
- 특정 조건을 만족할 때까지 스레드를 대기시켜야 할 때
- 다른 스레드의 작업 완료를 기다려야 할 때
- 생산자-소비자 패턴처럼 자원의 가용성에 따라 스레드를 대기/통지해야 할 때
- 락을 해제합니다 (다른 스레드가 락을 획득할 수 있게 됨)
- 현재 스레드를 대기 상태로 만듭니다
- 대기 상태에서 깨어나고
- 자동으로 다시 락을 획득합니다
- // 생산자 스레드 lock.lock(); try { while (queue.size() == capacity) { notFull.await(); // 1. 락을 해제하고 // 2. 대기 상태로 들어감 // (이 시점에 소비자가 락을 얻을 수 있음) } queue.add(element); // signal 받아서 깨어나면 // 1. 자동으로 락을 다시 획득하고 // 2. 이 라인부터 실행 notEmpty.signal(); } finally { lock.unlock(); }
- await()는 내부적으로 다음 두 가지를 자동으로 수행합니다:
- public class SimpleBlockingQueue<T> { private final ReentrantLock lock = new ReentrantLock(); private final Condition notFull = lock.newCondition(); private final Condition notEmpty = lock.newCondition(); private final Queue<T> queue = new LinkedList<>(); private final int capacity; public SimpleBlockingQueue(int capacity) { this.capacity = capacity; } public void put(T element) throws InterruptedException { lock.lock(); try { while (queue.size() == capacity) { // 큐가 가득 찼을 때는 대기해야 함 notFull.await(); } queue.add(element); notEmpty.signal(); } finally { lock.unlock(); } } public T take() throws InterruptedException { lock.lock(); try { while (queue.isEmpty()) { // 큐가 비었을 때는 대기해야 함 notEmpty.await(); } T item = queue.remove(); notFull.signal(); return item; } finally { lock.unlock(); } } }
- Condition이 실제로 필요한 경우는:
ReentrantLock의 메서드
- lock(): hold count가 1 증가하고 공유 리소스가 처음에 사용 가능한 경우 스레드가 락을 소유한다.
- 리소스가 사용중이면 획득할 때까지 블로킹된다.
- 같은 스레드가 여러 번 락을 획득할 수 있다(재진입 가능)
- unlock(): hold count를 1 감소시킨다. 이 카운트가 0에 도달하면 리소스가 해제된다.
- tryLock()
- 리소스가 비어 있으면 잠금을 획득하고 true를 반환한다(hold count는 1만큼 증가한다). 리소스가 비어 있지 않으면 메서드는 false를 반환한다.
- tryLock(long timeout, TimeUnit unit)
- 지정된 시간 동안 락 획득을 시도한다.
- 시간 내에 락을 획득하면 true, 시간 초과면 false를 반환한다.
- 대기 중 인터럽트되면 InterruptedException을 발생시킨다.
- lockInterruptibly()
- 리소스가 free 상태면 락을 얻는다. 리소스를 대기중에 다른 스레드가 해당 스레드를 인터럽트하는 것(현재 스레드에 대해 interrupt()를 호출)을 허용한다.
- 현재 스레드가 락을 기다리고 있는 동안, 다른 스레드가 현재 스레드의 interrupt()를 호출했을 때 InterruptedException이 발생한다.
- getHoldCount(): 리소스에 대해 held된 lock의 수를 반환한다.
- isHeldByCurrentThread(): 현재 스레드가 리소스에 대한 잠금을 갖고있는 경우 true를 반환
- hasQueuedThread(): 인수로 주어진 스레드가 잠금을 획득하기 위해 기다리고 있는지 여부를 쿼리한다.
- isLocked(): 잠금이 any 스레드에 의해 유지되고 있는지 여부를 반환한다.
- newCondition(): 이 Lock 인스턴스와 함께 사용할 Condition 인스턴스를 반환한다.
주의사항!
- unlock() 호출 보장을 위해 반드시 try-finally 블록을 사용할 것
- lock은 try 밖에, unlock은 finally에 있는 것을 명심하자.
- lock()을 try 내부로 넣게되면 lock() 호출에서 언체크 예외가 발생한 경우, unlock() 호출 시 lock을 획득하지 않은 상태여서 예외(IllegalMonitorStateException)가 발생한다. 실제 예외가 발생한 원인을 숨기게 된다.
- 락의 획득과 해제는 같은 스코프에서 이뤄져야 함
- 공정성 옵션의 사용은 성능에 영향을 미칠 수 있음
- 데드락 방지를 위해 락 획득 순서를 일관되게 유지
ReentrantReadWriteLock
대부분의 스레드가 공유 데이터를 읽기만 하고 쓰지 않을 때에는 ReentrantLock이 너무 보수적인 전략일 수 있다.
Java API는 ReentrantReadWriteLock으로 reader는 여러 개일 수 있지만 writer는 반드시 하나이어야 하는 락을 제공한다.
세마포, Semaphore
Java API는 카운팅 세마포를 제공한다
Semaphore(int value); // 세마포 생성자
- value는 세마포의 초기값을 지정한다(음수 값 허용)
- 락을 획득하려는 스레드가 인터럽트 되면 acquire() 메서드가 InterruptException을 발생시킨다
상호 배제를 위해 세마포를 사용하는 방법을 보여준다
Semaphore sem = new Semaphore(1);
try {
sem.acquire(); // InterruptedException 발생가능
// critical section
} catch (InterruptException ie) {
} finally {
sem.release();
}
마찬가지로 세마포가 반드시 해제될 수 있도록 release() 호출을 finally 안에 배치한다.
- InterruptedException은 Checked 예외이다.
조건변수, Condition Variables
ReentrantLock이 synchronized와 유사하듯이 조건변수는 wait()/notify() 메서드와 유사한 기능을 제공한다
따라서 상호 배제를 제공하려면 조건변수를 ReentrantLock과 연관시켜야 한다.
먼저 ReentrantLock을 생성하고 ReentrantLock::newCondition() 메서드를 통해 조건 변수를 생성한다.
이 메서드는 ReentrantLock의 조건 변수를 나타내는 Condition 객체를 반환한다.
Lock key = new ReentrantLock();
Condition condVar = key.newCondition();
조건변수를 통해 wait()/singal() 메서드를 사용한다
notify()를 통해 Java 스레드를 깨우면 깨어난 이유에 대한 정보를 받지 않는다.
대기 중인 조건이 충족되었는지 여부를 확인하는 것은 재활성화된 스레드 자신에게 달려있다. 조건 변수는 통지받을 특정 스레드를 지정할 수 있게하여 이를 해결한다.
아래 예제를 보자.
/* threadNumber is the thread that wishes to do some work */
public void doWork(int threadNumber) {
lock.lock();
try {
// If it's not my turn, then wait until I'm signaled.
if (threadNumber != turn) {
condVars[threadNumber].await();
}
// Do some work for awhile ...
// Now signal to the next thread.
turn = (turn + 1) % 5;
condVars[turn].signal();
} catch (InterruptedException ie) {
} finally {
lock.unlock();
}
}
Lock lock = new ReentrantLock();
Condition[] condVars = new Condition[5];
for (int i = 0; i < 5; i++) {
condVars[i] = lock.newCondition();
}
0~4까지 번호가 매개진 5개의 스레드와, 어떤 스레드의 차례인지를 나타내는 공유변수 turn이 있다고 하자.
위 코드에 따르면 threadNumber가 turn과 일치하는 스레드만 진행할 수 있다. 다른 스레드는 차례를 기다린다.
아래에서는 5개의 조건변수(스레드가 대기중인 조건)를 생성하여 다음 차례의 스레드에 신호할 수 있도록 Condition 배열을 준비한다.
스레드가 doWork()에 들어갈 때 threradNumber ≠ turn인 경우, 연관된 조건 변수에서 awiat() 메서드를 호출하고 다른 스레드가 신호를 보낼 때만 재개한다. 작업을 완료한 스레드는 다음 차레의 스레드에 연관된 조건 변수에 signal을 보낸다.
스레드가 조건 변수에서 await()를 호출하면 연관된 ReentrantLock이 해제되어 다른 스레드가 상호 배제 락을 획득할 수 있다. 이와 유사하게 signal()이 호출될 때 조건 변수에만 신호가 전달된다. 락은 unlock()을 호출하여 해제한다.
댓글