본문 바로가기

IT for developer/Java

Java Concurrency Part 2 – Reentrant Locks


원문: http://www.carfey.com/blog/?p=57

자바에서 synchronized 키워드는 탁월한 툴이다. 그러나 synchronization  이상의 것이 필요할 때가 있다. 접근 타입(읽기, 쓰기)를 제어할 필요가 있거나, mutext가 없거나 다중의 mutex를 관리할 필요하기 때문에 사용하는 것은 상당히 귀찮다.

Java Reentrant Locks

java.util.concurrent.locks 패키지에 몇개의 lock을 구현하고 있다.

Lock - 획득/해제할 수 있는 가장 간단한 락
ReadWriteRock - 읽기와 쓰기 락타입을 가지고 있다. 


ReentrantLock: 재진입 가능한 Lock 구현
ReentrantReadWriteLock: 재진입가능한 ReadWriteLock 구현


Read/Write Lock 예제


public class Calculator {
    private int calculatedValue;
    private int value;

    public synchronized void calculate(int value) {
        this.value = value;
        this.calculatedValue = doMySlowCalculation(value);
    }

    public synchronized int getCalculatedValue() {
        return calculatedValue;
    }

    public synchronized int getValue() {
        return value;
    }
}

일단 Lock을 사용하지 않고 syhchronized 를 사용한 경우이다. 만약에 상당히 많은 컨텐츠를 가지고 작업하는 경우이거나 대부분 쓰레드는 읽기를 수행하고 (getCalcuatedValue() 호출) 일부 쓰레드들만이 쓰기를 수행(calculate()호출) 한다고 하면 퍼포먼스가 상당히 떨어 질것이다. 어떤 쓰레드가 읽기 block(getCalculatedValue 함수) 에 들어가 있는 경우더라도 쓰기 쓰레드도 대기상태에 빠지게 된다. 이러한 문제를 최소화하기 위해 ReadWriteLock을 사용한다.

public class Calculator {
    private int calculatedValue;
    private int value;
    private ReadWriteLock lock = new ReentrantReadWriteLock();

    public void calculate(int value) {
        lock.writeLock().lock();
        try {
            this.value = value;
            this.calculatedValue = doMySlowCalculation(value);
        } finally {
            lock.writeLock().unlock();
        }
    }

    public int getCalculatedValue() {
        lock.readLock().lock();
        try {
            return calculatedValue;
        } finally {
            lock.readLock().unlock();
        }
    }

    public int getValue() {
        lock.readLock().lock();
        try {
            return value;
        } finally {
            lock.readLock().unlock();
        }
    }
}

명시적으로 읽기와 쓰기 락을 사용함으로써 synchronized 블럭을 사용하는 경우보다 좋은 퍼포먼스를 가질 수 있다.

다음 예제는 synchronized 와 명시적 Lock을 사용한 경우이다. 동일 쓰레드 클래스별로 Lock 객체를 한번만 생성한 후 작업하기 위하여 Lock객체를 생성하는 부분은 synchronized로 처리한 예제이다. tryLock은 aquire처럼 락을 얻을 때까지 무한 대기하는 것이아니라 일정 시간동안만 대기하가 설령 락을 얻디 못하였더라도 깨어난다.

public class TaskRunner {
    private Map<Class<? extends Runnable>,  Lock> mLocks =
            new HashMap<Class<? extends Runnable>,  Lock>();

    public void runTaskUniquely(Runnable r, int secondsToWait) {
        Lock lock = getLock(r.getClass());
        boolean acquired = lock.tryLock(secondsToWait, TimeUnit.SECONDS);
        if (acquired) {
            try {
                r.run();
            } finally {
                lock.unlock();
            }
        } else {
            // failure code here
        }
    }

    private synchronized Lock getLock(Class clazz) {
        Lock l = mLocks.get(clazz);
        if (l == null) {
            l = new ReentrantLock();
            mLocks.put(clazz, l);
        }
        return l;
    }
}