[JAVA] ReentrantLock 활용 Condition 인터페이스

ReentrantLock 공정성 설정

ReentrantLock은 두개의 생성자를 가지고 있습니다.

ReentrantLock()과 ReentrantLock(boolean fair)이 있습니다.

fair에 true를 적용시 공정성이 설정되어 Thread 작업이 오래 기다린 순서부터 접근 권한을 주는 방식입니다.(FIFO 방식)

 

ReentrantLock 공정성 설정 예시

ReentrantLock fairLock = new ReentrantLock(true); // 공정한 동기화 설정
ReentrantLock unfairLock = new ReentrantLock(); // 비공정한 동기화 (기본 설정)

Condition 인터페이스 개념

스레드 간의 통신을 가능하게 한다. Object에서 감시 메서드인(wait, notify, notifyAll)의 사용을 옮겨 놓았습니다. 그래서 스레드가 특정 조건을 만족할 때까지 대기하고, 조건이 충족되면 다른 스레드에게 신호를 보냅니다.

 

Condition 인터페이스 메서드

  • await() : 스레드를 일시적으로 대기 상태로 전환하고 조건이 발생할 때까지 대기합니다. 잠금 상태가 아닌데 await method를 호출 하면 IllegalMonitorStateException이 발생합니다.
  • signal() : 대기 중인 스레드 중 하나를 선택하여 깨우고 실행 가능 상태로 전환합니다.
  • signalAll() : 대기 중인 모든 스레드를 깨워 실행 가능 상태로 전환합니다.

Condition 인터페이스 생성

public class ConditionClass {
	private final Lock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();
}

ReentrantLock를 이용하여 newCondition method를 호출하여 Condition 인스턴스를 생성합니다.

 

await 예시

public void example() throws InterruptedException {
	lock.lock();
    try {
    	while(조건) {
        	condition.await();
        }
    } finally {
    	lock.unlock()
    }
}

조건식이 성립하면 운영중인 스레드는 await method가 호출되고 대기 상태가 되면서 잠금을 해제합니다. 대기 상태가 된 스레드는 깨어주는 신호가 올 때까지 기다립니다.

await에서 변수에 (시간, TimeUnit)을 주면 지정된 시간까지 기다린 후 깨어납니다.

 

signalAll 예시

public void example() {
	lock.lock();
    try {
    	condition.signalAll();
    } finally {
    	lock.unlock();
    }
}

condition waiting pool 안에 있는 스레드를 모두 깨어납니다. 그리고 다시 lock을 획득하기 위한 경쟁을 하게 됩니다.

 

Condition 인터페이스 예시

public class Main {
	public static void main(String[] args) throws InterruptedException {
		Counter counter = new Counter();
		Thread threadA = new Thread(() -> {
			while(true) {
				try {
					counter.increment();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		});
		Thread threadB = new Thread(() -> {
			while(true) {
				try {
					counter.decrement();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		});
		threadA.start();
		threadB.start();
	}
}
public class Counter {
	private final Lock lock = new ReentrantLock();
    private final Condition notZero = lock.newCondition();
    private final Condition notAtLimit = lock.newCondition();
    
    private final int limit = 100;
    private int count = 0;

    public int increment() throws InterruptedException {
        lock.lock();
        try {
            while (count == limit) {
                notAtLimit.await();
            }

            count++;
            notZero.signalAll();
            return count;
        } finally {
            lock.unlock();
        }
    }

    public int decrement() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                notZero.await();
            }

            count--;
            notAtLimit.signalAll();
            return count;
        } finally {
            lock.unlock();
        }
    }
}

하나의 counter 변수에서 증가 메서드와 감소 메서드로 실행중이기 때문에 ReentrantLock을 이용한 동기화 작업을 하였습니다. Condition을 이용한 2가지의 인스턴스를 만들었습니다.(notZero와 notAtLimit)

count가 증가하다가 limit에 도달하는 순간에 증가를 시도하는 모든 스레드들은 notAtLimit 컨디션에 await 상태에 들어갑니다. 그리고 notZero 컨디션 스레드를 깨우고 락 해제가 됩니다.

 

awaitUninterruptibly 예시

 public int example() {
        lock.lock();
        try {
            while (count == limit) {
                notAtLimit.awaitUninterruptibly();
            }

            count++;
            notZero.signalAll();
            return count;
        } finally {
            lock.unlock();
        }
    }

위에 예시에서 await에서 awaitUninterruptibly로 메서드가 바꼈습니다. await는 interrupts를 발생하지만 awaitUninterruptibly는 interrupts를 발생하지 않고 signal이 올 때까지 기다립니다.

 

signal과 signalAll 차이점

signal : 하나의 스레드만 깨웁니다.

signalAll : 모든 스레드를 깨웁니다.