synchronized
사용 시 발생하는 아래 문제점을 극복하기 위해 자바 1.5부터 Lock
인터페이스와 기본 구현체인 ReentrantLock
을 제공하게 되었다.
synchronized
한계BLOCKED
상태 유지BLOCKED
상태의 스레드 중 어느 스레드가 락을 획득할지 알 수 없음Lock 인터페이스 구현 시 Lock의 획득과 해제에 대하여 synchronized
방식에 비해 유연성이 증가하게 된다.
락의 해제 시 획득 순서와 동일하지 않아도 되는 등 보다 유연성을 가지지만 구현 시 책임을 가지게 된다.
package java.util.concurrent.locks;
import java.util.concurrent.TimeUnit;
/**
* (중략)
* @see ReentrantLock
* @see Condition
* @see ReadWriteLock
* @jls 17.4 Memory Model
*
* @since 1.5
*/
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
lock()
void lock();
lockInterruptibly()
void lockInterruptibly() throws InterruptedException;
lock()
과 달리 락 획득 대기 중 인터럽트 발생 시 InterruptedException
발생 후 락 획득 포기tryLock()
boolean tryLock();
tryLock(long time, TimeUnit unit)
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
InterruptedException
발생 후 락 획득 포기unlock()
void unlock();
newCondition()
Condition newCondition();
Condition
객체 생성 후 반환Condition
객체는 스레드가 특정 조건을 기다리거나 신호를 받을 수 있도록 함보통 아래와 같은 관용구를 사용하게 된다.
Lock l = ...;
l.lock();
try {
// 임계 구역: 락에 의해 자원 접근 시 보호됨
} finally {
l.unlock();
}
try~catch
, try~finally
로 보호되어 필요 시 잠금이 해제될 수 있도록 해야 한다.세 가지 형태의 락 획득 방식(인터럽트 가능/불가능, 타이밍 적용)은 성능/순서 보장 등에서 차이가 존재할 수 있다.
구현체는 세 가지 형태의 락 획득 방식에 대해 동일하게 보장할 필요가 없으며, 진행 중인 락 획득의 인터럽트를 지원할 의무도 없다.
락 획득 시 인터럽트를 지원하는 경우, 인터럽트는 락 획득 전체 과정에서 지원되거나, 메서드 진입 시에만 지원될 수 있다.
인터페이스를 직접 구현하여 사용할 수도 있지만 자바에서는 다양한 구현체를 제공한다.
대표적인 구현체는 아래와 같으며, 구현체마다 세부 구현사항이 다를 수 있어 사용 시 확인 후 사용하는 것이 좋다.
ReentrantLock
synchronized
메서드 및 구문을 통해 접근하는 모니터 락과 동일한 동작을 하며 추가 기능을 제공하는 재진입 가능한 상호 배제 락이다.
여기서 말하는 재진입은 동일한 스레드가 동일한 락을 여러 번 걸 수 있다는 의미이다.
마지막으로 락을 획득했지만 아직 해제하지 않은 스레드가 락을 소유하게 된다.
동일한 스레드 내 최대 2,147,483,647번의 재귀적 락을 지원하며, 한도 초과 시 락 메서드에서 Error
예외를 발생시킨다.
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;
... 중략...
public ReentrantLock() {
sync = new NonfairSync(); // 비공정 모드
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync(); // 매개변수가 true일 경우 공정 모드
}
... 중략...
}
true
로 넘길 경우, 경쟁 상황에서 가장 오랫동안 대기한 스레드에 락을 부여하는 것을 우선시하여 기아 현상이 발생하지 않도록 보장한다.tryLock()
메서드는 공정성을 따르지 않으며, 다른 스레드가 대기중이어도 락을 사용할 수 있으면 true
로 반환됨isHeldByCurrentThread()
public boolean isHeldByCurrentThread() {
return sync.isHeldExclusively();
}
true
, 아닐 경우 false
반환getHoldCount()
public int getHoldCount() {
return sync.getHoldCount();
}
class X {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
lock.lock();
try {
// 임계 영역
} finally {
lock.unlock();
}
}
}
ReentrantReadWriteLock
ReentrantLock
과 유사하지만 읽기/쓰기 전용 락을 각각 가지고 있다는 점에서 차이가 있다.
또한, 특정 종류의 컬렉션을 사용할 때 동시성 개선에 사용될 수 있다.
단, 컬렉션이 크고, 쓰기보다 읽기 작업이 많고 동기화 오버헤드보다 더 큰 오버헤드를 가지는 작업인 경우에만 유용하다.
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
private final ReentrantReadWriteLock.ReadLock readerLock
private final ReentrantReadWriteLock.WriteLock writerLock;
final Sync sync;
public ReentrantReadWriteLock() {
this(false); // 비공정 모드
}
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync(); // true일 경우 공정 모드
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
... 중략 ...
}
ReentrantLock
과 동일하게 공정/비공정 모드 선택이 가능하다. class CachedData {
Object data;
boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
if (!cacheValid) {
data = ...;
cacheValid = true;
}
rwl.readLock().lock();
} finally {
rwl.writeLock().unlock(); // 읽기 락 획득 후 쓰기락 해제를 통한 락 강등
}
}
try {
use(data);
} finally {
rwl.readLock().unlock();
}
}
}
StampedLock
읽기/쓰기 접근 제어를 위해 세 가지 모드를 제공하는 권한 기반의 락이며, 자바 1.8부터 제공되었다.
StampedLock
의 상태는 버전과 모드로 구성되고, 락 획득 메서드는 락 상태에 따른 접근을 제어할 수 있고 락의 상태를 나타내는 식별자인 스탬프를 반환한다.
락 해제 및 변환 메서드는 스탬프를 매개변수로 받아 락 상태와 일치하지 않으면 실패한다.
다른 구현체와 달리 Lock
또는 ReadWriteLock
인터페이스를 직접 구현하지 않았다.
public class StampedLock implements java.io.Serializable {
... 중략 ...
private transient volatile Node head;
private transient volatile Node tail;
transient ReadLockView readLockView;
transient WriteLockView writeLockView;
transient ReadWriteLockView readWriteLockView;
private transient volatile long state;
private transient int readerOverflow;
public StampedLock() {
state = ORIGIN;
}
... 중략 ...
}
스탬프를 어떤 방식으로 획득하였는지에 따라 아래와 같이 나뉜다.
writeLock()
사용 시readLock()
사용 시tryOptimisticRead()
사용 시tryConvertToWriteLock(long stamp)
메서드를 통해 아래 상황에서 쓰기 모드로의 업그레이드를 지원한다.
try~
메서드는 공정성을 보장하지 않을 수 있음