본문 바로가기
JAVA

MultiThread란? + Lock 사용 방법

by 슈슈슉민 2024. 6. 20.
jvm 은 multithread 를 지원한다.
thread는 cpu 작업 1단위이다.
multithread 방식은 1cpu에서 여러 thread 를 이용해 번갈아 처리하는 방식(context switching)이다.
여러 thread가  동시에 1 cpu 공유하기에 경쟁상태(raceCondintion) 문제가 발생한다.
thread: 아니, 동시에 들어왔잖아요!!

 

동시성을 제어하는 여러가지 방법들

  1. Lock: 한번에 하나의 스레스만이 자원에 접근할 수 있다.
  2. Semaphore: 공유 자원에 대한 접근 횟수를 제한하는 동기화 도구다. 세마포어는 카운터 값을 가지며, 이 값이 0 이하가 되면 더 이상 자원에 접근할 수 없다.
  3. mutex:  상호 배제(Mutual Exclusion)를 구현하는 동기화 도구다. 한 번에 오직 하나의 스레드만 공유 자원에 접근할 수 있도록 한다.
  4. monitor: Java의 동기화 메커니즘이다. 모든 객체에는 모니터가 있으며, 하나의 스레드만 모니터를 소유할 수 있다. synchronized 키워드를 사용하여 모니터를 획득하고 공유 자원에 접근할 수 있다.
  5. Barrier: 배리어는 여러 스레드가 특정 지점에서 동기화되도록 한다. 모든 스레드가 배리어에 도달할 때까지 기다렸다가 동시에 진행할 수 있다.
  6. Atomic: 원자적 연산은 더 이상 나누어질 수 없는 단일 단계로 수행되는 연산이다. 이러한 연산은 스레드 간섭 없이 안전하게 수행될 수 있다.
  7. Immutable: 불변 객체는 한 번 생성되면 그 상태를 변경할 수 없는 객체이다. 불변 객체는 스레드 안전성을 보장하므로 동시성 문제를 해결하는 데 도움이 된다.

어떤 방법이 적합할까

적절한 방법은 상황에 따라 다를 수 있다. 각 방법의 장단점을 고려하여 가장 적합한 방법을 선택해야 한다.

 
 

  1. Lock
    1. 장점: 직관적이며, 잘 알려진 메커니즘이다. 상대적으로 구현이 쉽다.
    2. 단점: 교착 상태(Deadlock)가 발생할 수 있다. 락을 획득하는 순서에 주의해야 한다.
    3. 적합한 상황: 공유 자원에 대한 상호 배제가 필요한 경우, 교착 상태 발생 가능성이 낮은 경우
  2. Semaphore
    1. 장점: 공유 자원에 대한 접근 횟수를 제한할 수 있다.
    2. 단점: 교착 상태가 발생할 수 있다(락과 마찬가지).
    3. 적합한 상황: 공유 자원에 대한 제한된 접근이 필요한 경우, 예를 들어 제한된 수의 연결 풀 관리 등
  3. Mutex
    1. 장점: 상호 배제를 보장한다. 교착 상태가 발생하지 않는다.
    2. 단점: 성능 저하가 발생할 수 있다.
    3. 적합한 상황: 크리티컬 섹션(Critical Section)이 짧고, 교착 상태 발생 가능성이 낮은 경우
  4. Monitor
    1. 장점: Java에서 기본적으로 제공되는 동기화 메커니즘이다.
    2. 단점: 교착 상태가 발생할 수 있다. 모니터 획득 순서에 주의해야 한다.
    3. 적합한 상황: 객체 수준의 동기화가 필요한 경우
  5. Barrier
    1. 장점: 여러 스레드를 동기화할 수 있다.
    2. 단점: 모든 스레드가 배리어에 도달할 때까지 기다려야 한다.
    3. 적합한 상황: 병렬 계산 등 여러 스레드가 동기화되어야 하는 경우
  6. Atomic
    1. 장점: 성능이 좋고, 교착 상태가 발생하지 않는다.
    2. 단점: 원자적 연산만 가능하다.
    3. 적합한 상황: 단순한 연산에 대한 동기화가 필요한 경우
  7. Immutable
    1. 장점: 스레드 안전성을 보장한다. 동기화가 필요 없다.
    2. 단점: 객체의 상태를 변경할 수 없다.
    3. 적합한 상황: 객체의 상태를 변경할 필요가 없는 경우

방법채택예시

상황: 돈을 결제가 동시에 일어난다.
주어진 상황에서는 요청에 대한 순차 처리가 필요하다. 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

'JAVA' 카테고리의 다른 글

람다란  (0) 2023.11.23
익명 클래스란  (0) 2023.11.23