- 경쟁 상태 (Race Condition)
- 한 개의 자원에 여러 프로세스가 접근하려고 하는 상태
- 결과를 예측하는 것이 불가능함
- 데이터의 일관성을 확보할 수 없음
- 임계구역 (Critical Section)
- 공유 데이터를 변경하는 코드 영역
- 해당 영역으로의 접근을 제어함으로써, 프로세스 간 경쟁 상태 문제를 해결할 수 있다.
- 임계구역 문제를 해결하는 세 가지 조건
- 상호 배제(Mutual Exclusion)
- 한 프로세스가 임계구역에 들어가면 다른 프로세스는 임계구역에 들어갈 수 없도록 해야한다.- 한정 대기(Bounded Waiting)
- 임계 구역에서 작업 중인 프로세스가 나오기를 기다리면서 무한 대기를 하면 안 되며, 일정 시간 이내에 다른 프로세스가 임계구역에 들어갈 수 있도록 해야한다.- 진행의 융통성(progress flexibility)
- 한 프로세스가 임계구역에 들어가 있으면 다른 프로세스가 강제로 그 자리를 빼앗을 수 없도록 해야한다.
import java.util.concurrent.atomic.AtomicBoolean;
public class SpinLock {
private AtomicBoolean isLocked = new AtomicBoolean(false);
public void lock() {
while (!isLocked.compareAndSet(false, true)) {
// isLocked 값이 false이면 true로 설정하고, 이전 값이 false인 경우에만 루프를 빠져나옵니다.
Thread.yield(); // 다른 스레드에게 실행을 양보합니다.
}
}
public void unlock() {
isLocked.set(false);
}
}
스핀락은 반복문을 돌면서 계속해서 임계 구역에 진입 가능 여부를 확인한다.
계속해서 반복문을 돌기 때문에 성능적으로 비효율적이다. 그러나 특정한 경우, 컨텍스트 스위칭 시간이 더 짧거나 멀티 코어 프로세스일 경우 대기시간보다 응답시간이 짧아서 더 빠른 성능을 가질 수 있다.
public class CriticalSection {
private int count = 0;
private Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
}
increment() 메소드는 임계구역에 접근하는 코드이다. lock() 메소드를 호출하여 잠금을 획득하고, 임계구역에 접근한 후에는 unlock() 메소드를 호출하여 잠금을 해제한다.
이를 통해 여러 스레드가 동시에 increment() 메소드에 접근하더라도 한 번에 하나씩 실행되도록 보장할 수 있다.
public class SharedResource {
private Semaphore semaphore = new Semaphore(1); // 카운팅 세마포어 생성
public void accessResource() {
try {
semaphore.acquire(); // 세마포어 값을 감소시킴
// 공유 자원에 대한 작업 수행
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 세마포어 값을 증가시킴
}
}
}
생성자에는 해당 세마포어의 초기값을 전달한다.
위 코드에서는 카운팅 세마포어를 생성하고, 초기값으로 1을 전달하였다.
accessResource() 메서드에서는 공유 자원에 대한 작업을 수행하기 전에 semaphore.acquire() 메소드를 호출하여 세마포어 값을 1 감소시키고, 작업을 마치고 finally 블록에서는 semaphore.release() 메소드를 호출하여 세마포어 값을 1 증가시킨다.
이를 통해 다른 스레드가 공유 자원에 접근할 수 있도록 한다.
import java.util.concurrent.atomic.AtomicBoolean;
public class TestAndSetExample {
private AtomicBoolean lock = new AtomicBoolean(false); // 초기값은 false
public void criticalSection() {
while (lock.getAndSet(true)); // lock이 false인 경우 true로 변경하고, 그렇지 않은 경우 lock이 true가 될 때까지 대기
// 임계 구역 시작
// ...
// 임계 구역 종료
lock.set(false); // lock을 false로 변경하여 다른 스레드가 임계 구역에 진입할 수 있도록 함
}
}
AtomicBoolean 클래스를 사용하여 lock 변수를 생성한다. lock 변수는 임계 구역에 진입할 때 사용하는 변수로, 초기값은 false로 설정된다.
getAndSet(true) 메소드를 사용하여 lock 변수의 값을 true로 변경한다. 이때, getAndSet 메소드는 원래 lock 변수의 값을 반환하고, true로 값을 변경한다.
만약 반환된 값이 false이면 while문을 빠져나가고, true이면 while문에서 대기한다.
즉, lock 변수가 false인 경우에만 임계 구역에 진입할 수 있다.
임계 구역을 벗어나면 lock 변수를 다시 false로 변경하여 다른 스레드가 임계 구역에 진입할 수 있도록 한다.