임계영역(Critical Section)
- 여러 스레드가 공유하는 자원에 접근하는 코드 영역을 말하는 논리 영역이다
- 멀티 스레딩 환경에서 여러 스레드가 동시에 접근할 때 문제가 발생할 수 있는 영역이다
- 이 영역에서는 공유 자원에 대한 접근이 이루어지기 때문에 동시에 접근하면 데이터 불일치 문제가 발생할 수 있다
- 공유 자원의 예로는 전역 변수, 파일, 데이터베이스 등이 있다
임계영역 특징
- 상호 배제 (Mutual Exclusion)
- 한 스레드가 임계영역에서 실행 중일 때, 다른 스레드는 해당 임계영역에 진입할 수 없어야 한다
- 한정된 대기 (Bounded Waiting)
- 스레드가 임계영역 진입하기 위해 대기하는 시간은 한정되어야 한다
- 즉, 무한정 대기하는 상황이 발생하면 안 된다
- 진행 가능성
- 임계영역에 진입하려는 스레드가 없다면, 진입을 원하는 스레드는 반드시 진입할 수 있어야 한다
- 공유 자원 보호
- 임계영역은 공유 자원의 상태를 일관성 있게 유지하기 위해 사용된다
- 데드락 회피
- 임계영역을 잘못 구현하면 두 개 이상의 스레드가 서로를 기다리며 무한 대기 상태에 빠지는 상황이 발생할 수 있다
임계영역을 구현하는 방법
- synchronized 키워드
- Lock 객체
- lock(), unlock() 사이의 코드 블록이 임계영역이 된다
- ReentrantLock을 가장 많이 사용한다
- Lock 객체의 구현체로 이름처럼 재진입이 가능한 락이다
- 즉, 동일한 스레드가 이미 락을 획득한 상태에서 다시 락을 요청할 수 있다
- 공정성 옵션을 줄 수 있는데 생성자에 true를 주는 방식이다.
- 이 옵션을 주면 대기 중인 스레드 중 가장 오래 기다린 스레드에게 락을 부여한다
public class Counter {
private int count = 0;
private Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
- ReadWriteLock
- 읽기 작업과 쓰기 작업을 분리하여 동시 읽기 접근을 허용한다
- ReentrantReadWriteLock을 사용하면 여러 스레드가 동시에 읽기 작업을 수행할 수 있다
- 쓰기 작업은 단일 스레만 수행할 수 있다
public class Counter {
private int count = 0;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
public void increment() {
lock.writeLock().lock();
try {
count++;
} finally {
lock.writeLock().unlock();
}
}
public int getCount() {
lock.readLock().lock();
try {
return count;
} finally {
lock.readLock().unlock();
}
}
}
- Atomic 변수
- 원자적 연산으로 락이 없어도 동기화를 제공한다
public class Counter {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
- Semaphore
- 지정된 수의 스레드만 임계영역에 접근할 수 있게 한다
- 자원 풀과 같은 곳에서 사용할 수 있다
public class Resource {
private final Semaphore semaphore = new Semaphore(1);
public void access() {
try {
semaphore.acquire();
// 임계영역
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
semaphore.release();
}
}
}
- StampedLock
- ReadWriteLock과 비슷하지만, 낙관적 잠금(optimistic lockign) 기능을 제공한다
- 낙관적 잠금
- 자원이 충돌하지 않을 것이라고 가정하고 작업을 진행하는 방식
- 데이터에 접근할 때 락을 사용하지 않고, 대신 변경이 완료되기 전에 데이터가 수정되지 않았는지 확인한다
- 이를 위해서 주로 버전 번호나, 타임스탬프를 사용한다
- 충돌이 발생하면 작업을 다시 시도한다
- volatile 키워드
- volatile 키워드는 원자성을 보장하지 않고 가시성만 보장한다
- 가시성이란 한 스레드가 volatile 변수를 수장하면, 다른 스레드에서는 항상 수정된 값을 읽게 된다