자바 SE 1.5 에 도입된 새로운 동기화 기능을 제공하는 Lock

예전에 본 어떤 책에서는 Before/After 패턴으로 소개하고 있었는데 이 Lock 클래스를 사용하는 방식에 그대로 적용된다.
lock.lock();
try {
    // processing context critical section
} catch ( Exception e ) {
    ....
}finally {
    lock.unlock();
}

lock.lock()에서 권한을 회득할 때까지 현재 스레드는 block 된다.

이후 이미 lock을 얻은 스레드고 unlock()을 호출하면 block 된 스레드들이 runnable 상태로 돌아가서 lock 을 놓고 경쟁을 벌인다.

한 스레드만 lock을 획득하고 Critical Section 을 수행한 후 finally 구문에 의해서 역시 unlock() 이 호출됨을 보장받는 식으로 동기화를 수행한다.

간단한 예제로 "숫자탑 쌓기" 프로그램을 만들어서 사용법을 익혀봄.

이 프로그램은 0 에서 n-1 까지 n 개의 숫자를 임의로 섞어서 NumberPart 라는 클래스들에게 임의로 할당한다. m 개의 NumberPart 클래스는 정렬이 되지 않은 숫자 집합들을 가지게 되고 자신들이 가진 숫자를 순서에 맞게 차곡차곡 NumberTower 에 쌓는 식.

이를 위해서 동기화가 이뤄져야 하는데 각 NumberPart는 현재 NumberTower에 올려진 숫자를 보고 자신이 그 다음 숫자를 가지고 있는지 확인한 후 가지고 있으면 그 숫자를 반환한 수 lock을 다른 클래스들에게 넘기는 식으로 수행된다.

곰곰히 생각해보면 이 예제는 동기화가 필요하지 않다. 왜냐하면 현재 타워에 올려진 i 다음 숫자인 i+1을 가진 NumberPart 스레드는 단 하나이기 때문. 물론 구현을 바꾸면 동기화가 필요할 수도 있지만 lock의 사용법을 익혀보기 위해서 만들어본 예제임.

스 레드간 동기화를 조율하는 코드는 한 클래스 내에 몰아넣는게 편하다. 예제에서도 NumberPart에는 아무런 동기화 관련 코드가 없다. 실제로 동기화를 관리하는 NumberTower 에 lock, unlock 코드가 몰려있다.

lock_test.zip

여러개의 스레드가 Runnable을 상속한 이 클래스를 동시에 실행하고 NumberPart에서는 tower에 계속해서 숫자 쌓기를 시도한다.

Class NumberPart implements Runnable {
    private NumberTower tower ;

    public void run() {
        while ( numbers.size() > 0 ){
            tower.pile(this); // 계속해서 숫자 쌓기를 시도한다.
        }
        System.out.println(this.name +"'s Lock Count : " + lockGainedCount);
    }
}

NumberTower 클래스 안에 모든 동기화 관련 코드가 들어있다.
class NumberTower {

    final private Lock lock = new ReentrantLock();

    public void pile ( NumberPart np ){
        lock.lock();
        try {
            if ( np.hasValue(current )){
                np.remove(current);
                current++;
            }
            if ( current == top )
                timeNotificationCondition.signalAll();
        } finally {
            lock.unlock();
        }
    }
}

만일 숫자탑 쌓기 과정이 진행된 시간을 알고 싶다면 어떻게 해야할까?

여 러개의 NumberPart 스레드가 탑을 다 쌓을때까지 기다려야 한다.

concurrent 패키지가 제공되지 않을때는 시간을 체크하는 스레드를 만들어서 두어서 NumberTower 에 대한 모니터를 획득한 후
    while ( current < top ) {
        monitor.wait();
    }

와 같은 식으로 WAIT 상태에서 기다리고 이 스레드를 깨우기 위해서 NumberTower나 NumberPart가 스레드에 대한 참조를 가지고 있다가 인터럽트를 걸어주는 식으로 구현했었다.

java.util.concurrent 패키지에서는 java.util.concurrent.locks.Condition 인터페이스를 제공하고 있는데 이 클래스가 바로 WAIT 의 기능을 대체하는 역할을 한다.

Condition 인스턴스는 하나의 Lock 에 대해서 여러개를 생성할 수 있는데 동기화를 관리하는 클래스가 자신에게 접근하는 스레드들의 목적이나 용도에 따라서 서로 다른 Condition 에서 기다리게 할 수 있다.

예전에는 모니터의 WAIT 상태에 있던 스레드들을 하나만 깨우거나 모두 다 깨워야만 했지만 condition 인스턴스를 통해서 깨우고 싶은 스레드들의 집합만 별도로 깨울 수가 있다.

관련 예제는 자바문서에 제공되고 있는데

http://java.sun.com/javase/6/docs/api/java/util/concurrent/locks/Condition.html

BoundedBuffer 에 접근하는 두 집단의 스레드들(데이터를 넣는 스레드들, 데이터를 빼내는 스레드들)에게 각각 별도의 Condition 인스턴스에서 WAIT 하게 한다.

빼갈 데이터가 없는 상태에서 데이터를 빼가려는 스레드가 접근할 경우 notEmpty 에서 WAIT 하게 한다. 나중에 데이터를 넣는 스레드들 중 하나가 lock을 얻어서 버퍼에 데이터를 입력한 후 notEmpty.signalAll(); 을 호출하면 WAIT하고 있던 스레드들이 모두 runnable 상태로 돌아간다.(이 스레드들은 모두 데이터를 빼내려는 스레드들이다).

이 기능을 NumberTower에 적용해서 숫자탑 쌓는데 걸린 시간을 출력하려는 스레드를 WAIT 상태에서 묶어놓을 수 있다.
public class ReentrantLockTest {
    .......
    public static void main(String[] args) {
        .........
        tower.getElapsedTime();
    }
}

위에서 getElapedTime(); 을 호출하는 스레드는 메인 스레드임에 유의해야함.

메인스레드는 경과 시간을 출력하기 위해서 lock을 얻은 후 곧바로 WAIT 상태로 들어간다.
class NumberTower {
    public void getElapsedTime(){
        lock.lock();
        try {
            timeNotificationCondition = lock.newCondition();
            while ( current < top ) {
                System.out.println("wating..............");
                timeNotificationCondition.await();
            }
            System.out.println("elapsed " + (System.currentTimeMillis() - startTime));
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            lock.unlock();
        }
    }
}

탑 쌓기가 다 끝난 후
class NumberTower {

    public void pile ( NumberPart np ){
        lock.lock();
        try {
            if ( np.hasValue(current )){
                np.remove(current);
                current++;
            }
            if ( current == top )
                timeNotificationCondition.signalAll();
        } finally {
            lock.unlock();
        }
    }
    ....
}

condition 인스턴스의 signallAll(); 을 호출해서 메인스레드를 runnable 상태로 되돌린다. while 루프를 빠져나왔을때는 이미 탑쌓기가 끝났으므로 경과 시간을 출력할 수 있다.

1.5 이전에 Object.wait(); 관련 메소드를 호출하기 위해서 먼저 모니터를 획득해야하는 것처럼 위에서도 lock을 획득한 후에 java.util.concurrent.locks.Condition.await(); 를 호출해야한다.(안그러면 예외 발생)

Posted by yeori
,