jvm 은 multithread 를 지원한다.
thread는 cpu 작업 1단위이다.
multithread 방식은 1cpu에서 여러 thread 를 이용해 번갈아 처리하는 방식(context switching)이다.
여러 thread가 동시에 1 cpu 공유하기에 경쟁상태(raceCondintion) 문제가 발생한다.

동시성을 제어하는 여러가지 방법들
- Lock: 한번에 하나의 스레스만이 자원에 접근할 수 있다.
- Semaphore: 공유 자원에 대한 접근 횟수를 제한하는 동기화 도구다. 세마포어는 카운터 값을 가지며, 이 값이 0 이하가 되면 더 이상 자원에 접근할 수 없다.
- mutex: 상호 배제(Mutual Exclusion)를 구현하는 동기화 도구다. 한 번에 오직 하나의 스레드만 공유 자원에 접근할 수 있도록 한다.
- monitor: Java의 동기화 메커니즘이다. 모든 객체에는 모니터가 있으며, 하나의 스레드만 모니터를 소유할 수 있다. synchronized 키워드를 사용하여 모니터를 획득하고 공유 자원에 접근할 수 있다.
- Barrier: 배리어는 여러 스레드가 특정 지점에서 동기화되도록 한다. 모든 스레드가 배리어에 도달할 때까지 기다렸다가 동시에 진행할 수 있다.
- Atomic: 원자적 연산은 더 이상 나누어질 수 없는 단일 단계로 수행되는 연산이다. 이러한 연산은 스레드 간섭 없이 안전하게 수행될 수 있다.
- Immutable: 불변 객체는 한 번 생성되면 그 상태를 변경할 수 없는 객체이다. 불변 객체는 스레드 안전성을 보장하므로 동시성 문제를 해결하는 데 도움이 된다.
어떤 방법이 적합할까
적절한 방법은 상황에 따라 다를 수 있다. 각 방법의 장단점을 고려하여 가장 적합한 방법을 선택해야 한다.
- Lock
- 장점: 직관적이며, 잘 알려진 메커니즘이다. 상대적으로 구현이 쉽다.
- 단점: 교착 상태(Deadlock)가 발생할 수 있다. 락을 획득하는 순서에 주의해야 한다.
- 적합한 상황: 공유 자원에 대한 상호 배제가 필요한 경우, 교착 상태 발생 가능성이 낮은 경우
- Semaphore
- 장점: 공유 자원에 대한 접근 횟수를 제한할 수 있다.
- 단점: 교착 상태가 발생할 수 있다(락과 마찬가지).
- 적합한 상황: 공유 자원에 대한 제한된 접근이 필요한 경우, 예를 들어 제한된 수의 연결 풀 관리 등
- Mutex
- 장점: 상호 배제를 보장한다. 교착 상태가 발생하지 않는다.
- 단점: 성능 저하가 발생할 수 있다.
- 적합한 상황: 크리티컬 섹션(Critical Section)이 짧고, 교착 상태 발생 가능성이 낮은 경우
- Monitor
- 장점: Java에서 기본적으로 제공되는 동기화 메커니즘이다.
- 단점: 교착 상태가 발생할 수 있다. 모니터 획득 순서에 주의해야 한다.
- 적합한 상황: 객체 수준의 동기화가 필요한 경우
- Barrier
- 장점: 여러 스레드를 동기화할 수 있다.
- 단점: 모든 스레드가 배리어에 도달할 때까지 기다려야 한다.
- 적합한 상황: 병렬 계산 등 여러 스레드가 동기화되어야 하는 경우
- Atomic
- 장점: 성능이 좋고, 교착 상태가 발생하지 않는다.
- 단점: 원자적 연산만 가능하다.
- 적합한 상황: 단순한 연산에 대한 동기화가 필요한 경우
- Immutable
- 장점: 스레드 안전성을 보장한다. 동기화가 필요 없다.
- 단점: 객체의 상태를 변경할 수 없다.
- 적합한 상황: 객체의 상태를 변경할 필요가 없는 경우
방법채택예시
상황: 돈을 결제가 동시에 일어난다.
주어진 상황에서는 요청에 대한 순차 처리가 필요하다. Lock 또는 Semaphore를 사용하는 것이 적절하다.
Lock일 경우: 각 스레드는 Lock을 획득한 후 특정 메서드를 실행하고, 작업이 완료되면 Lock을 해체한다. 단 deadLock이 발생할 수 있다.
Semaphore일 경우: Semaphore는 공유 자원에 대한 접근 횟수를 제한할 수 있다. 이 경우 Semaphore 의 허용 개수를 1로 설정하면 한 번에 하나의 스레드만 메서드에 접근할 수 있다. 이럴 경우 Lock과 비슷하게 사용 가능하다. Lock과 다른 점은 2개이상의 허용 개수 설정이 가능하다는 점이다.
둘다 성능측면에서는 어느 정도의 오버체드가 발생할 수 있다.
상호 배제란 둘 이상의 프로세스가 동시에 임계 영역(CS : Critical Section)에 진입하는 것을 방지하기 위해 사용되는 알고리즘이다.
@Test
@DisplayName("동시 요청에 대한 순차 처리 확인 테스트 케이스")
void chargeConcurrentPoints() throws InterruptedException {
// given
long userId = 1L;
int numThreads = 10;
long chargingPoints = 100L;
long expectedTotalAmount = numThreads * chargingPoints;
CountDownLatch countDownLatch = new CountDownLatch(numThreads);
ExecutorService executorService = Executors.newFixedThreadPool(numThreads);
// when
IntStream.range(0, numThreads).forEach(e -> executorService.submit(() -> {
try {
pointService.chargePoints(userId, chargingPoints);
} finally {
countDownLatch.countDown();
}
}
));
countDownLatch.await();
executorService.shutdown();
// then
final Long afterPoint = pointService.getPoints(userId).point();
assertEquals(expectedTotalAmount, afterPoint);
}
test code
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Service
@RequiredArgsConstructor
public class PointService {
private final UserPointRepository userPointRepository;
private final PointHistoryRepository pointHistoryRepository;
private final Lock lock = new ReentrantLock();
// ... 기존 메서드들 ...
public UserPoint chargePoints(long userId, long amount) {
lock.lock(); // Lock 획득
try {
return userPointRepository.insertOrUpdate(userId, amount);
} finally {
lock.unlock(); // Lock 반환
}
}
}
lock 적용 예시
import java.util.concurrent.Semaphore;
@Service
@RequiredArgsConstructor
public class PointService {
private final UserPointRepository userPointRepository;
private final PointHistoryRepository pointHistoryRepository;
private final Semaphore semaphore = new Semaphore(1); // 동시 접근 허용 개수를 1로 설정
// ... 기존 메서드들 ...
public UserPoint chargePoints(long userId, long amount) throws InterruptedException {
semaphore.acquire(); // Semaphore 획득
try {
return userPointRepository.insertOrUpdate(userId, amount);
} finally {
semaphore.release(); // Semaphore 반환
}
}
}
semaphore