[Java] concurrent.locks

자바 프로그래밍을 하다보면 동시에 다른 명령을 여러 개 수행해야 하는 동시성(concurrency)작업을 해야 할 일이 많이 생깁니다. 자바에서는 이러한 동시성작업을 해야하는 멀티쓰레드 환경에서 버그 없이 작성하게 도와주는 많은 객체들이 있습니다. java.util.concurrent 패키지에는 ConcurrentHashMap, ConcurrentLinkedQueue, ConcurrentSkipListMap 등 동시성(concurrency)을 보장하는 다양한 컬랙션을 제공합니다.

이번 포스팅에서는 이 중 lock을 관리하는 java.util.concurrent.locks중에서도 ReentrantLock에 대하여 살펴보려합니다. java.util.concurrent.locks의 구현체를 크게 보자면 Lock 인터페이스를 구현하는 ReentrantLock과 ReadwriteLock을 구현하는 ReentrantReadWriteLock으로 볼 수 있습니다. ReadwriteLock의 경우에는 읽기때에는 동시에 여러 스레드가 읽을 수 있도록 락을 유지하고 쓸때는 하나의 스레드만 접근 할 수 있도록 만든 락입니다. 그래서 ReentrantLock과 기본적인 동작과 개념은 유의하기 때문에 ReentrantLock에 대하여 알아보겠습니다.

synchronized와 ReentrantLock

흔히 동기화를 구현할 때 synchronized 키워드를 사용해서 구현합니다. 그렇다면 같은 동기화 기능을 지원하는 클래스인 ReentrantLock은 어느 경우에 쓰이는 것일까요? 둘과의 비교를 통해 알아봅시다.

Lock의 경우 명시적락과 암묵적 락이 있는데 synchronized의 경우 암묵적 락, ReentrantLock은 명시적인 락이라고 합니다. 먼저 synchronized는 동기화를 하고자하는 블럭이나 메소도를 synchronized로 감싸서 락을 겁니다.

List<String> list = new ArrayList<String>();
...
private boolean threadA(){
    synchronized(list){
        list.add("Lock in A");
    }
}
...
private boolean threadB(){
    synchronized(list){
        list.add("Lock in B");
    }
}

이 경우 두개의 스레드가 공유하는 list라는 컬랙션에 락을 체택하여 구현합니다. 만약 동기화 코드 내에 list뿐만 아니라 다른 2차컬랙션이 있다고 하더라도 주요 컬랙션을 락으로 동기화를 해줍니다. 그래서 이러한 작업들은 어느 부분이 락인지 명확하지 않아서 암묵적인 락이라고 합니다.

명시적인 락은 앞의 연산과 같은 경우를 구현할 때 다중 스레드가 공유하는 주요 컬랙션 대신 완전히 독립적인 락을 채택하여 구현하는 것입니다. Lock interface를 구현하는 RenntrantLock을 이용해 synchronized와 동일한 기능을 구현합니다.

Lock reentrantLock = new ReentrantLock();
List<String> list = new ArrayList<String>();

private boolean A(){
    try{
        reentrantLock.lock();
        list.add("Lock");
    }
    finally{
        reentrantLock.unlock();
    }
}

private boolean B(){
    try{
        reentrantLock.lock();
        list.add("Lock");
    }
    finally{
        reentrantLock.unlock();
    }
}

.lock()메소드는 호출은 락을 획득하게 되고 한 번 락이 스레드에 의해 획득되면 다른 스레드는 락이 풀릴때까지 락 블록 내부를 실행 할 수 없습니다. .unlock()으로 인해 락이 풀리기 되면 기다리고 있던 스레드 중 단 하나만이 다시 락을 획득하게 됩니다. RenntrantLock의 경우 synchronized만으로는 해결 할 수 없는 복잡한 경우에 쓰이게 됩니다.

  • synchronized의 경우 기본적으로 스레드간의 락을 획득하는 순서를 보장해주지 않습니다. 이러한 것을 불공정 방법이라고 하는데 RenntrantLock은 불공정방법뿐만 아니라 메소드를 이용해 순서를 보장해 주도록(공정방법)으로 설정 할 수 있습니다.
  • 앞의 예제와 같이 코드가 단일 블록의 형태를 넘어 여러가지 컬랙션이 얽혀 있을때 명시적으로 락을 실행시킬 수 있습니다.
  • 대기상태의 락에 대한 인터럽트를 걸어야 할 경우
  • 락을 획득하려고 대기중인 스레드들의 상태를 받아야 할 경우에 쓸 수 있습니다.

예전 자바 1.5시절에는 synchronized가 더 빠르고 reentrantlock은 쓰레드 덤프조차 뜰 수 없었다고 하지만 1.6부터는 해결되었고 점차 차이가 없어지고 있습니다. 포스팅 밑에 링크한 블로그 글에 따르면 4개 이상부터는 reentrantlock이 더 효율이 좋다고 합니다(jdk 1.6). 다만 RenntrantLock을 쓸 경우 기본 키워드인 synchronized과 달리 java.util.concurrent를 import해야 되고 try/fianlly block이 무조건 들어가기 때문에 코드가 지저분해지는 단점등이 있습니다.
그래서 아래와 같은 경우를 제외하고는 간단한 동기화 코드를 작성 할 때는 synchronized가 더 낫다고 결론 지을 수 있습니다.

  • 락을 모니터링 해야 할 때
  • 락을 획득하려는 쓰레드의 개수가 많을 때(4개 이상)
  • 위에서 이야기한 복잡한 동기화 코드를 작성해야 할 때

ReentrantLock의 메소드

ReentrantLock락은 fair/unfair lock을 생성자로 boolean변수 값을 받아 구현합니다.

 private final Sync sync;

/**
 * Base of synchronization control for this lock. Subclassed
 * into fair and nonfair versions below. Uses AQS state to
 * represent the number of holds on the lock.
 */
abstract static class Sync extends AbstractQueuedSynchronizer {
    abstract void lock();
    ..... 생략 
}

/**
 * Creates an instance of {@code ReentrantLock}.
 * This is equivalent to using {@code ReentrantLock(false)}.
 */
public ReentrantLock() {
    //비공정 방식으로 초기화 

    sync = new NonfairSync();
}

/**
 * Creates an instance of {@code ReentrantLock} with the
 * given fairness policy.
 *
 * @param fair {@code true} if this lock should use a fair ordering policy
 */
public ReentrantLock(boolean fair) {
    //boolean 방식에 따라 초기화 

    sync = fair ? new FairSync() : new NonfairSync();
}

NonfairSync와 FairSync가 static class로 reentrantLock 내부에서 Sync를 상속받아 구현 되어있습니다. 이 sync는 추상화 클래스로 lock method만 Fairsync와 nonFailSync가 구현하고 나머진 Sync와 AbstractQueuedSynchronizer를 상속받아서 구현되어 있습니다. ReentrantLock 내부에서는 스레드 컨트롤을 Sync를 통해 합니다.
Fairsync와 nonFailSync는 lock메소드와 acquireLock 즉 락을 하고 락을 얻을 수 있는지 여부에 관하여 다르게 구현되어 있습니다.

/**
 * Sync object for non-fair locks
 */
static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;

    /**
     * Performs lock.  Try immediate barge, backing up to normal
     * acquire on failure.
     */
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1); //내부에서 tryAcquire를 호출합니다.

    }
    //대기중인 스레드가 있는지 검사하여 락을 획득할수 있는지를 돌려줍니다. nonfairTryAcquire에 관하여 아래 한번더 설명이 되어 있습니다.

    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

/**
 * Sync object for fair locks
 */
static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

    final void lock() {
        acquire(1); //내부에서 tryAcquire를 호출합니다.

    }

    /**
     * Fair version of tryAcquire.  Don't grant access unless
     * recursive call or no waiters or is first.
     */
     // NonFairSync와 거의 일치하지만 차이점은 hasQueuedPredecessors()라는 검사를 한다는 것입니다. 

        그래서 해당 큐가 락을 획득  첫번째 스레드인지를 검사해줍니다. 
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}



ReentrantLock class의 메스드를 살펴 보면 ReentrantLock는 lock 인터페이스를 상속 받아서 구현합니다. lock과(sync.lock()) unlock()을 구현하고 있습니다. 또한, ReentrantLock의 주요 메서드가 바로 trylock()이라는 메서드 입니다. trylock은 락을 선점한 스레드가 없을 때만 락을 얻으려고 시도하는 메서드입니다.

이 메서드를 통해 락을 획득하는 경우 스레드는 대기상태에 빠지지 않습니다. trylock의 retrun값은 boolean으로 락을 획득한 경우는 true 실패할 경우는 false를 반환합니다. 또한 lock을 획득하는 대기시간을 지정 할 수도 있습니다. 이 메서드를 통해 lock을 획득 할 경우 실행 코드와 없을 경우를 분리 시켜 획득하지 못했다 하더라도 스레드가 lock을 획득하려고 대기상태에 빠지지 않게 되는 것입니다.

public boolean tryLock() {
    return sync.nonfairTryAcquire(1);
}
/**
 * Performs non-fair tryLock.  tryAcquire is implemented in
 * subclasses, but both need nonfair try for trylock method.
 */
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    // 현재 락을 얻은 스레드가 있는지 검사하고 없으면 true를 반환합니다.

    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 락을 얻은 쓰레드가 자신일 경우 true를 반환합니다. 

    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow

            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    // 그 이외에는 false를 반환합니다. 

    return false;
}

모니터링을 위한 대기중인 스레드와 개수(getQueuedThreads,getQueueLength), 현재 락을 획득한 스레드, 특정 조건에서의 thread를 검사해주는 메서드 등을 제공하고 있습니다.
그 중 몇가지를 예로 살펴보면

// 대기중인 큐가 있는지 검사합니다.

public final boolean hasQueuedThreads() {
    return sync.hasQueuedThreads();
}
//...sync내의 메서드 queue의 해드와 테일이 같은지를 판별해서 대기중 쓰레드의 존재 여부를 리턴해줍니다.

public final boolean hasQueuedThreads() {
    return head != tail;
}
// 조건에 맞는 대기중인 큐를 리턴해줍니다.

protected Collection<Thread> getWaitingThreads(Condition condition) {
if (condition == null)
    throw new NullPointerException();
if (!(condition instanceof AbstractQueuedSynchronizer.ConditionObject))
    throw new IllegalArgumentException("not owner");
return sync.getWaitingThreads((AbstractQueuedSynchronizer.ConditionObject)condition);
}

protected final Collection<Thread> getWaitingThreads() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    ArrayList<Thread> list = new ArrayList<Thread>();
    for (Node w = firstWaiter; w != null; w = w.nextWaiter) {
        if (w.waitStatus == Node.CONDITION) {
            Thread t = w.thread;
            if (t != null)
                list.add(t);
        }
    }
    return list;
}

참고

Comments

comments powered by Disqus
comments powered by Disqus