최근에 Redisson 을 사용한 프로젝트 과제를 하면서 IllegalMonitorStateException이 발생하는 경우가 생겼다.
마침 이전에 궁금했던 wait,와 notify 그리고 Monitor에 관해 확인한 내용을 적어본다
wait()와 notify()는 동기화상태에서 스레드간 신호를 보내기 위해 사용된다. 여기서 신호란 스레드를 대기상태인 스레드를 실행상태로 바꾸거나(notify, notifyAll), 실행중인 스레드를 대기상태로 바꾸는것(wait)이다.
두개의 스레드가 BlockingQueue를통해 데이터를 주고 받는 코드를 구현할때, 두개의 스레드사이에서 신호의 전달이 필요하다.
public abstract class BaseBoundedBuffer<V> {
/**
* Queue에 데이터를 추가한다
*
*/
abstract void put(E e) throws InterruptedException;
/**
* Queue에서 데이터를 가져온다
*/
abstract V take() throws InterruptedException;
}
/**
* 질나쁜(Grumpy) Bounded Buffer. 동기화 클래스 지만 예외가 발생하면, 그 예외를 던져서 호출자가 처리하게 한다.
* @param <V>
*/
public class GrumpyBoundedBuffer<V> extends BaseBoundedBuffer<V> {
/**
*
* @param v 추가값
* @throws BufferFullException 버퍼가득찬 상태라 예외발생. 호출자에게 예외 처리를 넘긴다. 호출자는 예외처리를 해야한다.
*/
public synchronized void put(V v) throws BufferFullException {
if (isFull())
throw new BufferFullException();
doPut(v);
}
/**
* take를 시도하되 버퍼가 비어있다면 예외를 던진다. 호출자는 이를 처리해야 한다.
* @throws BufferEmptyException 버퍼가 비어있음 예외.
*/
public synchronized V take() throws BufferEmptyException {
if (isEmpty())
throw new BufferEmptyException();
return doTake();
}
}
이 경우 코드에 나와있듯, 호출자가 예외를 처리해야한다.
while(true){
try{
V item = buffer.take();
break;
} catch(BufferEmptyException e){
//버퍼가비었다. 1초 대기후 재실행,
//호출하는 쪽에서는 예외처리 코드를 추가해야한다.
//만약 대기Thread.sleep 없이 다시 take를 호출하면 CPU자원 소모량이 증가한다(spin waiting)
//그렇다고 대기시간을 길게 잡으면 필요이상으로 시간이 지연되여 응답성에 손해를 본다.
Thread.sleep(1000);
}
}
/**
* {@link concurrent.ex1.GrumpyBoundedBuffer} 에서 예외를 던졌다면, 여기서는 예외를 발생하지 않게, 직접 처리(Sleep)한다.
* 하지만 Thread sleep 의 시간에 따라 CPU 사용량이 크게 올라가거나, 응답속도가 떨어진다.
* @param v
*/
@Override
public void put(V v) throws InterruptedException {
while (true) {
synchronized (this) {
if (!isFull()) {
doPut(v);
return;
}
}
Thread.sleep(1000);
}
}
@Override
public V take() throws InterruptedException {
while (true) {
synchronized (this) {
if (!isEmpty()) {
return doTake();
}
}
Thread.sleep(1000);
}
}
두가지 방법의 공통점은 put과 take의 호출과 선행조건이 연결되있지 않다는 것이다. 즉 put은 선행조건(여유공간이 있는가?)의 확인을 put메소드를 실행하고 나서야 체크한다. 만약 선행조건을 만족한다면 진행되지만, 그렇지 않다면 임의의 시간만큼 스레드를 멈추고(sleep) 다시 선행조건을 확인한다.
public class BoundedBuffer<V> extends BaseBoundedBuffer<V> {
public BoundedBuffer(int capacity) {
super(capacity);
}
@Override
public synchronized void put(V e) throws InterruptedException {
//스레드가 멈추는 조건
while (isFull()) {
wait();
}
doPut(e);
notifyAll();
}
@Override
public synchronized V take() throws InterruptedException {
while (isEmpty()) {
wait();
}
V v = doTake();
notifyAll();
return v;
}
}
코드로 나타내면 위와 같다.
1,2 와 3번의 가장 큰 차이는 쓰레드간에 신호를 주고받는지 여부일 것이다. 1,2는 신호를 주고받지 않고, 예외처리와 대기시간을 통해 처리한다. 반면 3은 wait와 notifyAll을 사용하여 스레드간의 신호를 주고받는다.