java / / 2023. 11. 15. 19:48

java.util.concurrent.Locks

아래 내용은 https://www.baeldung.com/java-concurrent-locks 의 내용을 정리한 글입니다.

1. 개요

lock은 표준 sychronized 블록보다 좀 더 유연하고 정교한 쓰레드 동기화 메커니즘이다.

Lock 인터페이스는 Java 1.5부터 지원한다. java.util.concurrent.lock 패키지에 있고 locking을 위한 다양한 기능을 제공한다.

2. Lock과 Sychronized Block의 차이점

sychronized 블록과 Lock API를 사용것의 차이점이 몇 가지 있다.

  • sychronized block은 온전히 메소드 내에서만 가능하다. Lock API의 lock()unlock()을 사용해서 다른 메소드에서 사용할 수 있다.
  • sychronized block은 공정성(fairness)을 지원하지 않는다. 해제된 락(lock)을 어떤 쓰레드라도 획득할 수 있고 특정 설정이 없다. Lock API내에서 적절한 설정을 명시함으로써 공정성(fairness) 있게 사용될 수 있다. 가장 오래 기다린 쓰레드가 락을 획득할 수 있도록 할 수 있다.
  • sychronized block에 접근할 수 없다면 쓰레드는 대기한다. Lock API는 tryLock() 메소드를 제공한다. 쓰레드가 가능하고 다른 쓰레드가 관여되지 않다면 해당 쓰레드가 락을 획득한다. 이것은 락을 대기하는 쓰레드의 시간을 줄여준다.
  • synchronized block에 접근하려고 대기하는 쓰레드는 중지(interrupt)될 수 없다. Lock API는 락을 대기할 때 쓰레드를 중지하기 위해 lockInterruptibly() 메소드를 제공한다.

3. Lock API

Lock 인터페이스에 있는 메소드를 한번 보자.

  • void lock() - 사용이 가능하다면 락을 획득한다. 락이 가용하지 않다면 쓰레드는 락이 해제될 때까지 대기한다.
  • void lockInterruptibly() - lock()과 비슷하지만 대기 중인 쓰레드를 중지할 수 있고 java.lang.InterruptedException을 통해 실행을 다시 재개할 수 있다.
  • boolean tryLock() - lock() 메소드의 nonblocking() 버전이다. 락 획득을 즉시 시도하고 가용상태면 true를 리턴한다.
  • boolean tryLock(long timeout, TimeUnit timeUnit) - tryLock()과 비슷하고 락 획득 시도를 포기하기 전 timout만큼 대기한다.
  • void unlock() - 락 인스턴스를 해제한다.

락 인스턴스는 데드락을 피하기 위해 항상 해제되어야 한다.

락을 사용하는데 가장 좋은 방법은 try/catchfinally을 포함하는 것이다.

Lock lock = ...; 
lock.lock();
try {
    // access to the shared resource
} finally {
    lock.unlock();
}

Lock 인스턴스에 읽기 전용과 쓰기 전용 쌍으로 구성된 lock을 관리하는 ReadWriteLock 인터페이스가 있다. 쓰기 작업이 없는 한 readLock은 동시에 여러 쓰레드에서 사용될 수 있다.

ReadWriteLockdms 읽기를 획득하거나 lock을 쓰기 위한 메소드를 선언한다.

  • Lock readLock()은 읽기 전용 락이다.
  • Lock writeLock()은 쓰기 전용 락이다.

4. Lock 구현

4.1 ReentrantLock

ReentrantLock 클래스는 Lock 인터페이스를 구현한다. synchronized 메소드 및 구문를 사용하여 접근하는 Lock을 모니터링하는 것과 같이 동시성 제어 기능을 제공한다.

다음은 동기화를 위해 ReentrantLock를 사용하는 방법이다.

public class SharedObject {
    //...
    ReentrantLock lock = new ReentrantLock();
    int counter = 0;

    public void perform() {
        lock.lock();
        try {
            // Critical section here
            count++;
        } finally {
            lock.unlock();
        }
    }
    //...
}

데드락을 피하기 위해 try-finally 블럭에서 lock()과 unlock()을 래핑하고 있다.

public void performTryLock(){
    //...
    boolean isLockAcquired = lock.tryLock(1, TimeUnit.SECONDS);

    if(isLockAcquired) {
        try {
            //Critical section here
        } finally {
            lock.unlock();
        }
    }
    //...
}

이 케이스에서 tryLock()을 호출하는 쓰레드는 1초간 대기하고 락 획득이 되지 않으면 대기를 중지할 것이다.

4.2 ReentrantReadWriteLock

ReentrantReadWriteLock 클래스는 ReadWriteLock 인터페이스를 구현한다.

쓰레드에서 ReadLock 혹은 WriteLock을 획득하는 규칙을 한번 보자.

  • Read Lock - write lock을 획득하거나 요청한 쓰레드가 없다면, 여러 쓰레드가 read lock을 획득할 수 있다.
  • Write Lock - 어떤 쓰레드도 읽기나 쓰기작업을 하지 않는다면 오직 하나의 쓰레드만 write lock을 획득할 수 있다.
public class SynchronizedHashMapWithReadWriteLock {

    Map<String,String> syncHashMap = new HashMap<>();
    ReadWriteLock lock = new ReentrantReadWriteLock();
    // ...
    Lock writeLock = lock.writeLock();

    public void put(String key, String value) {
        try {
            writeLock.lock();
            syncHashMap.put(key, value);
        } finally {
            writeLock.unlock();
        }
    }
    ...
    public String remove(String key){
        try {
            writeLock.lock();
            return syncHashMap.remove(key);
        } finally {
            writeLock.unlock();
        }
    }
    //...
}

양쪽 쓰기 작업을 하는 메소드에 write lock을 중요 로직 주변에 사용해야 한다 - 하나의 쓰레드만 접근할 수 있도록

Lock readLock = lock.readLock();
//...
public String get(String key){
    try {
        readLock.lock();
        return syncHashMap.get(key);
    } finally {
        readLock.unlock();
    }
}

public boolean containsKey(String key) {
    try {
        readLock.lock();
        return syncHashMap.containsKey(key);
    } finally {
        readLock.unlock();
    }
}

양쪽 읽기 메소드에 read lock으로 중요 로직 주변에 사용해야 한다. 진행 중인 쓰기 작업이 없다면 여러 쓰레드가 이 영역에 접근할 수 있다.

4.3 StampedLock

StampedLock은 Java 8에서 도입되었다. 읽기, 쓰기 락을 지원한다.

하지만 락(lock) 획득 메소드는 락을 해제하거나 여전히 유효한지 체크하는데 사용된 stamp를 리턴한다.

public class StampedLockDemo {
    Map<String,String> map = new HashMap<>();
    private StampedLock lock = new StampedLock();

    public void put(String key, String value){
        long stamp = lock.writeLock();
        try {
            map.put(key, value);
        } finally {
            lock.unlockWrite(stamp);
        }
    }

    public String get(String key) throws InterruptedException {
        long stamp = lock.readLock();
        try {
            return map.get(key);
        } finally {
            lock.unlockRead(stamp);
        }
    }
}

StampedLock에서 제공하는 또 다른 특징은 낙관적(optimistic) 락이다. 대부분의 시간에서, 읽기 작업은 쓰기 작업이 완료될 때까지 기다릴 필요가 없고 그 결과로서 완전한 읽기 락은 필요하지 않다.

public String readWithOptimisticLock(String key) {
    long stamp = lock.tryOptimisticRead();
    String value = map.get(key);

    if(!lock.validate(stamp)) {
        stamp = lock.readLock();
        try {
            return map.get(key);
        } finally {
            lock.unlock(stamp);               
        }
    }
    return value;
}
Copy

5. 조건에 따른 동작

Condition 클래스는 크리티컬 섹션(critical section)을 실행하는 동안 특정 조건에 대해 쓰레드가 대기하는 기능을 제공한다.

쓰레드가 크리티컬 섹션에 접근하지만 동작을 수행하는데 필요한 조건을 가지고 있지 않을 때 발생한다. 예를 들면 reader 쓰레드는 소비(consume)할 어떤 데이터도 가지고 있지 않는 공유 큐(queue)의 락에 접근할 수 있다.

전통적으로 java는 쓰레드 통신하는데 wait(), notify(), notifyAll() 메소드를 제공한다.

Conditions는 비슷한 매커니즘을 가지고 있지만 여러 조건을 명시할 수 있다.

public class ReentrantLockWithCondition {

    Stack<String> stack = new Stack<>();
    int CAPACITY = 5;

    ReentrantLock lock = new ReentrantLock();
    Condition stackEmptyCondition = lock.newCondition();
    Condition stackFullCondition = lock.newCondition();

    public void pushToStack(String item){
        try {
            lock.lock();
            while(stack.size() == CAPACITY) {
                stackFullCondition.await();
            }
            stack.push(item);
            stackEmptyCondition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public String popFromStack() {
        try {
            lock.lock();
            while(stack.size() == 0) {
                stackEmptyCondition.await();
            }
            return stack.pop();
        } finally {
            stackFullCondition.signalAll();
            lock.unlock();
        }
    }
}

참고

https://www.baeldung.com/java-concurrent-locks

반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유