1. 잠금 사용 시 주의사항

 

잠금 해제하기

잠금을 획득한 뒤에는 반드시 잠금을 해제해야 한다. 그렇지 않으면 잠금을 시도하는 스레드가 무한정 대기하게 된다. 세마포어도 마찬가지다. 퍼밋을 획득했다면 반드시 퍼밋을 반환해야 한다. 반환하지 않으면 퍼밋을 얻으려는 스레드는 끝없이 대기하게 된다.

 

lock.lock();
try {
    // 코드
} finally {
    lock.unlock(); // finally는 항상 실행되므로 잠금을 무조건 해제한다.
}

잠금을 사용할 때는 위처럼 finally 블록에서 잠금을 해제하는 코드를 작성하는 습관을 들이자.

 

대기시간 지정하기

잠금 획득을 시도하는 코드는 잠금을 구할 수 있을 때까지 계속 대기하는데, 동시 접근이 많아지면 대기 시간이 길어지는 문제가 발생할 수 있다. 이를 막기위한 방법 중 하나는 대기 시간을 지정하는 것이다. 다음은 잠금 획득 제한 시간을 5초로 설정한 예시이다.

boolean acquired = lock.tryLock(5, TimeUnit.SECONDS); // 5초 대기
if (!acquired) {
    // 잠금 획득 실패
    throw new RuntimeException("Failed to acquire lock");
}
// 잠금 획득 성공
try {
    // 자원 접근 코드 실행
} finally {
    // 잠금 해제
    lock.unlock();
}

위 코드에서 tryLock() 메서드는 5초 동안 잠금 획득을 시도하고 5초 이내 잠금을 획득하면 true를 반환하고, 실패하면 false를 반환한다. 잠금을 획득하지 못하면 자원 접근 코드를 실행하지 않고 실패에 대한 처리를 수행한다.

 

잠금 획득 대기 시간 없이 바로 결과를 반환하는 tryLock() 메서드를 사용할 수도 있다.

boolean acquired = lock.tryLock(); // 대기 시간 없이 바로 결과 반환
if (!acquired) {
    // 잠금 획득 실패
    throw new RuntimeException("Failed to acquire lock");
}

 

긴 응답 시간은 사용자 경험에 유익하지 않기에 요청을 빨리 처리할 수 있으면 하되, 안된다면 적절한 대기시간을 설정해 실패 응답이라도 가능한 빨리 사용자가 받을 수 있도록 하는 것이 좋다.

 

교착상태(deadlock) 피하기

교착 상태는 2개 이상의 스레드가 서로가 획득한 잠금을 대기하면서 무한히 기다리는 상황을 의미한다. 그 예시는 다음과 같다.

 

교착 상태가 발생하지 않도록 신경 써야 하지만, 복잡한 코드 구조에서 잠금을 사용하면 개발자 자신도 모르게 교착 상태 상황이 발생할 수 있다. 그걸 해소할 수 있는 방법은 2가지가 있는데, 잠금 대기 시간 제한지정한 순서대로 잠금 획득이다.

 

  • 잠금 대기 시간 제한
    • 잠금 대기 시간을 5초로 제한하면 교착상태가 발생하더라도 5초 뒤에 잠금 획득에 실패하면서 교착상태가 풀리게 된다.
  • 지정한 순서대로 잠금 획득
    • 예시) 정렬 순서 기준으로 잠금 시도일 경우, 스레드1과 스레드2 모두 먼저 자원A 잠금을 획득하려할 것이고 만약 스레드1이 자원 A를 먼저 점유했다면 스레드2는 잠금 해제를 대기한다. 이후 스레드2가 자원A를 반환한 후 스레드2가 자원A 잠금을 획득하고 스레드1이 다음 자원B 잠금을 획득함으로써 교착상태를 예방할 수 있다.
     

 

 

 

라이브락(livelock)

라이브락은 활동을 하는 것 가티만 실제로는 아무것도 하지 않는 상태를 의미한다. 해소 방법으로는 우선순위 선정, 중재자, 임의성(임의의 시간만 대기 후 이동)이 있다.

 

기아 상태(starvation status)

우선 순위가 높은 작업이 많아 우선 순위가 낮은 작업이 실행이 안 되거나 특정 자원을 한 프로세스가 긴 시간동안 독점하고 있어 다른 프로세스가 자원에 접근하지 못해 이후 작업을 실행하지 못하는 경우가 있을 수 있다. 이렇게 프로세스나 스레드가 자원을 할당받지 못해 실행되지 못하는 상태를 기아 상태라고 한다.

 

해결방법으로는 실행 안 되고 있는 작업의 우선순위를 높이거나, 공유자원의 독점 제한 시간을 설정하는 방법이 있다.

 

2. 단일 스레드로 처리하기

동시성 문제가 발생하는 주된 이유는 여러 스레드가 동시에 동일한 자원에 접근하기 떄문이다. 이를 방지하기 위해 잠금과 같은 수단을 사용하는데, 잠금을 잘못 사용하면 교착상태 같은 상황이 발생할 수 있기에 주의해서 구현해야한다. 이렇듯 동시성은 문제와 해결방식 모두 고려할 부분이 많은데, 복잡한 구현 없이 동시성 문제를 해결할 수 있는 방법이 있다. 바로 한 스레드만 자원에 접근하는 방식을 사용하는 것이다. 여러 스레드가 자원에 접근을 하지 않으니 애초에 동시성 문제가 발생하지 않는다.

 

위에서 상태 관리 스레드만 데이터를 조작한다. 데이터 변경이나 접근이 필요한 스레드는 작업 큐에 필요한 작업을 추가할 뿐 직접 상태에 접근하지 못한다. 상태 관리 스레드는 아래 코드처럼 작업 큐에서 작업을 꺼내어 필요한 데이터 처리를 수행한다.

while (running) {
    // 한 스레드만 큐에서 작업을 꺼내서 실행한다.
    Job job = jobQueue.poll(1, TimeUnit.SECONDS);
    if (job == null) {
        continue;
    }
    
    // job 종류에 따라 상태 처리
    switch(job.getType()) {
        case INC : 
            obj.modifyState(); // modifyState()는 한 스레드만 접근하므로 동시성 문제가 없다
            break;
        // 다른 작업
    }
}

 

 

만약 서로 다른 두 스레드가 데이터 공유가 필요할 경우 콜백이나 큐 같은 수단을 사용해서 데이터 복제본이나 불변(immutable) 값을 공유하기도 한다. 이때 복제본이나 불변 값을 공유하는 이유는 다른 스레드에서 데이터를 수정하지 못하게 해 동시성 문제가 발생하는 것을 막기 위함이다.

 

  • 단일 스레드 사용 장점
    • 동시성 문제를 고려하지 않아도 된다
  • 단일 스레드 사용 단점
    • 구조는 복잡해지는 단점이 있다. 이 점을 고려해서 단일 스레드 사용을 검토해야 한다.

참고로 논블로킹이나 비동기 IO를 사용하는 경우에는 블로킹 연산을 최소화해야 하므로 단일 스레드 처리 방식이 적합하다.

 

단일 스레드 vs. 멀티 스레드

단일 스레드 멀티 스레드

동시에 실행되는 작업 개수가 많고, 임계 영역의 실행시간이 긴 경우에는 단일 스레드 방식이 더 적합할 수 있다.

이 상황에서 단일 스레드의 성능이 멀티 스레드 성능과 비슷하거나 더 좋아지기 때문이다.
동시 실행 작업 개수가 적고, 임계 영역의 실행시간이 짧다면 멀티스레드 + 잠금 방식이 적합할 수 있다.

큐/채널 처리 시간이 잠금 획득/해제 시간보다 저 걸릴 수 있기에 단일 스레드 방식이 멀티 스레드 방식보다 성능이 떨어질 수 있기 때문이다.

 

+ Recent posts