[Java][Thread] 쓰레드의 동기화 #2 Lock과 Condition을 이용한 동기화

2022. 7. 5. 18:28JAVA/Language

JDK에서 동기화할 수 있는 방법으로 synchronized 키워드를 제외한 "java.util.concurrent.locks" 패키지를 제공합니다. locks 클래스는 JDK 1.5 이후부터 제공합니다.

 

1. lock 클래스의 종류

  • ReentrantLock : 재진입이 가능한 lock, 가장 일반적인 배타 lock
  • ReentrantReadWriteLock : 읽기에는 공유적이고, 쓰기에는 배타적인 lock
  • StampedLock : ReentrantReadWriteLock에 낙관적인 lock의 기능들을 추가
    • StampedLock 클래스는 JDK 1.8부터 추가됨
    • Lock 인터페이스를 구현하지 않음

 

ReentrantLock 클래스

  • 특정 조건에서 lock을 풀고 나중에 다시 lock을 얻고 임계영역으로 들어와서 이후의 작업을 수행할 수 있습니다.
  • 임계 영역의 공유자원을 읽거나 또는 쓰기 위해서는 무조건 lock을 가지고 있어야 합니다.

 

ReentrantReadWriteLock 클래스

  • 읽기를 위한 lock과 쓰기를 위한 lock을 제공합니다.
  • ReentrantLock 클래스와의 차이점은 ReentrantLock 클래스의 lock은 배타적이기 때문에 무조건 lock을 가지고 있어야만 임계영역의 코드를 수행할 있습니다.
  • ReentrantReadWriteLock 클래스는 읽기 lock이 걸려 있으면 다른 쓰레드가 읽기 lock을 중복해서 걸고 읽기를 수행할 수 있습니다. 그러나 읽기 lock이 걸려 있는 상태에서 쓰기 lock을 거는 것은 허용되지 않습니다.

 

StampedLock 클래스

  • lock을 걸거나 해지할 때 '스탬프(long타입의 정수값)'를 사용하며, 읽기와 쓰기를 위한 lock 외에 '낙관적 읽기 lock(optimistic reading lock)'이 추가된 것입니다.
  • 일반적인 읽기 lock이 걸려있으면, 쓰기 lock을 얻기 위해서는 읽기 lock이 풀릴때까지 대기해야합니다.
  • 낙관적 읽기 lock이 걸려있으면, 쓰기 lock에 의해 바로 풀리게 됩니다. 그래서 낙관적 읽기에 실패하면, 읽기 lock을 얻어서 다시 읽어 와야합니다.
  • 따라서 StampedLock 클래스는 무조건 읽기 lock을 걸지 않고, 쓰기와 읽기가 충돌할 때만 쓰기가 끝난후에 읽기 lock을 거는 것입니다.

 

다음의 코드는 일반적인 StampedLock을 이용한 낙관적 읽기의 예제입니다.

int getBalance(){
	long stamp = lock.tryOptimisticRead(); // 낙관적 읽기 lock을 건다.
    
    int curBalance = this.balance; // 공유 데이터인 balance를 읽어온다.
    
    if(!lock.validate(stamp)){ // 쓰기 lock에 의해 낙관적 읽기 lock이 풀렸는지 확인
        stamp = lock.readLock(); // lock이 풀렸으면, 읽기 lock을 얻으려고 기다린다.
        
        try{
            curBalance = this.balance;
        }finally{
            lock.unlockRead(stamp); // 읽기 lock을 푼다
        }
    }
    
}

 

2. ReentrantLock 클래스

ReentrantLock의 생성자

ReentrantLock()
ReentrantLock(boolean fair)
  • fair = true : lock이 풀렸을 때 가장 오래 기다린 쓰레드가 lock을 얻을 수 있게함. 하지만 성능은 떨어지게 됨

 

ReentrantLock의 lock 관련 메서드

void lock()        : lock을 잠근다
void unlock()      : lock을 해지한다
boolean isLocked() : lock이 잠겼는지 확인한다

boolean tryLock()  : 다른 쓰레드에 의해 lock이 걸려 있으면 lock을 얻으려고 대기하지 않음
boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException
                   : tryLock과 동일하나 지정된 시간만큼만 대기함
  • lock() 메서드는 lock을 얻을때까지 쓰레드를 블록(block) 시킵니다.
  • tryLock() 메서드는 lock을 얻으면 true를 반환, 얻지 못하면 false를 반환
  • tryLock(long timeout, TimeUnit unit) throws InterruptedException : 지정된 시간동안 lock을 얻으려고 기다리는 중에 interrupt()에 의해 작업을 취소될 수 있도록 코드를 작성할 수 있다는 의미입니다.

 

임계 영역 내에서 예외가 발생하거나 return문으로 빠져 나가게되면 lock이 풀리지 않을 수 있으므로 unlock() 메서드는 try-finally문으로 감싸는 것이 일반적입니다.

lock.lock(); // ReentrantLock lock = new ReentrantLock();
try{
	// 임계영역
}finally{
	lock.unlock();
}

 

3. ReetrantLock과 Condition

wait() & notify() 메서드의 문제점

  • 대기중인 특정한 쓰레드를 지정하여 깨우지 못합니다.
  • notify() 호출시 대기중인 임의의 쓰레드를 깨우게 됩니다.

 

Condition

  • 쓰레드의 종류를 구분하지 않고 공유 객체의 waiting pool에 몰아 넣는 대신 특정한 종류의 쓰레드를 위한 Condition을 만들어서 waiting pool에서 따로 기다리도록 만들 수 있습니다.

 

Condition 인스턴스 생성

private ReentrantLock lock = new ReentrantLock(); // lock 생성

// lock으로 condition을 생성
private Condition forCook = lock.newCondition();
private Condition forCust = lock.newCondition();

 

Object 인스턴스와 Condition 인스턴스의 동기화 관련 메서드 비교

Object Condition
void wait() void await()
void awaitUninterruptibly()
void wait(long timeout) boolean await(long time, TimeUnit unit)
long       awaitNanos(long nanosTimeout)
boolean awaitUntil(Date deadline)
void notify() void signal()
void notifyAll() void signalAll()

 

References

source code : https://github.com/yonghwankim-dev/java_study/blob/main/ch13/ex_25_locks/Driver.java
[도서] Java의 정석, 남궁 성 지음