아래 내용은 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/catch와 finally을 포함하는 것이다.
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();
}
}
}