TIL

임계영역을 알아보고 어떤 방법으로 구현할 수 있을까?

삼공비 2024. 6. 18. 22:33

임계영역(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 변수를 수장하면, 다른 스레드에서는 항상 수정된 값을 읽게 된다