[Java][Thread] 쓰레드의 동기화 #2 Lock과 Condition을 이용한 동기화
2022. 7. 5. 18:28ㆍJAVA/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의 정석, 남궁 성 지음
'JAVA > Language' 카테고리의 다른 글
[Java][Thread] 쓰레드의 동기화 #4 fork & join 프레임워크 (0) | 2022.07.05 |
---|---|
[Java][Thread] 쓰레드의 동기화 #3 volatile (0) | 2022.07.05 |
[Java][Thread] 쓰레드의 동기화 #1 synchronized, wait, notify (0) | 2022.07.01 |
[Java][Thread] 쓰레드의 실행 제어 (0) | 2022.06.30 |
[Java][Thread] 데몬 쓰레드(daemon thread) (0) | 2022.06.30 |