[백엔드] 동기화/동시성 문제(한 작업에 대한 병렬처리) 해결을 위한 개념5 - 스레드를 관리하기 위한 기본 개념(스레드 상태관리, 동기화)

Hyo Kyun Lee·3일 전
0

백엔드

목록 보기
16/16

1. 개요

스레드 상태를 직접적으로 관리할 수 있는 메소드를 공부한 내용을 기록한다.

2. 스레드 상태

스레드는 실행대기 - 실행 - 종료의 생명주기를 가진다.

  • 스레드의 실행대기는 start() 메서드를 통해 이루어진다.
  • 그 후 자원을 할당받아 정의한 작업 내용대로 처리하며, 이는 run() 메서드를 통해 이루어진다.
  • run() 메서드 종료 후 실행 종료된다.

이 상태체계를 정리하면 하기와 같다.

  1. 객체생성(NEW) : 스레드 객체 생성, 실행대기 전에 스레드를 만드는 작업
  2. 실행대기(RUNNABLE) : 스레드 실행대기(실행상태로 갈 수 있는) 상태, start()
  3. 통지대기(WAITING) : 다른 스레드가 작업완료를 통지(notify)할 때까지 기다리는 상태
  4. 시간대기(TIME_WAITING) : 주어진 시간 동안 기다리는 상태
  5. 블록(BLOCKED) : 다른 스레드가 자원을 점유하고 있는 동안 접근금지상태에서 기다리는 상태(LOCK)
  6. 종료(TERMINATED) : 스레드의 작업이 종료된 상태

3. 스레드 상태제어

스레드 상태제어는 스레드 각각의 상태를 제어하는 것이 아니라, 스레드의 흐름을 제어하는 static(정적) 제어임을 명확히 이해한다.

  • sleep : 스레드를 지정 시간동안 일시정지한다(static(정적) 메서드 이며, 특정 객체를 제어하는 것이 아닌 스레드 흐름 자체를 제어하기 위한 용도로 사용하며 최종적으로는 스레드 그룹을 제어하는 것과 동일한 의미)
  • interrupt : 일시정지인 상태인 스레드를 실행대기 상태로 전환한다.
  • join : 정해진 시간동안 지정 스레드가(*피작업 대상이 아닌 작업대상에게 시간을 부여) 작업하는 것을 기다리도록 일시정지 상태로 전환한다(시간을 지정하지 않았다면 해당 작업이 종료될때까지 대기하며, 메인스레드 역시 해당 스레드 종료까지 대기 후 작업을 완료한다).
  • yield : 양보하다는 의미, 즉 멀티스레드 구동 상태일때 특정 스레드를 yield 하여 다른 스레드에게 자원을 양보하면서 자신은 실행대기 상태로 전환하는 과정을 진행한다.
  • wait : notify와 같이 활용, 실행상태에서 작업완료 통지까지 기다리도록 waiting pool에서 대기하는데, 구체적으로는 콘서트 티켓과 같이 더이상 작업진행이 의미가 없을 경우 wait()을 통해 lock을 반납하고 일시정지 상태로 전환하도록 한다.
  • notify : 다시 작업을 진행할 수 있는 상황이라면 notify()를 통해 다시 실행가능상태로 전환한다(즉, 다시 LOCK을 얻어 로직을 진행할 수 있다).

4. 멀티스레드 동기화 - synchronized(임계영역 설정)

멀티 스레드의 경우 자원을 공유하는데, 이로 인해 자원 교착상태(데드락) 및 자원경쟁시점의 차이로 인한 정합성이 저해되는 상황이 발생할 수 있다.

이러한 충돌상황이나 오류를 방지하기 위해 동기화(synchronized) 작업을 진행해야 한다.

DBMS 차원에서 자원 자체에 대한 접근을 막는 것을 비관락 혹은 낙관락이라 하며, 애플리케이션 스레드 혹은 로직(쉽게 말하면 메서드) 차원의 접근을 막는 것을 임계영역 설정이라 한다.

임계영역에는 Lock을 가진 하나의 스레드만 출입이 가능하며, 이를 설정할 수 있는 방법에는 다음과 같은 방법이 존재한다.

  • 메서드 전체를 임계영역으로 지정한다(synchronized 키워드 사용).
  • 특정(참조변수) 영역을 임계영역으로 설정한다(synchronized(참조변수)).

이때 참조변수는 스레드 내부에서, 해당 스레드 자체를 지칭하는 this로도 인수 활용이 가능하다.

5. 교착상태

notify, wait의 경우 스레드의 작업대기 및 실행전환 시점이 맞지 않거나 특정 스레드의 자원점유로 인해 다른 스레드의 자원점유가 진행되지 않을 경우, 자원점유가 이루어지지 않는 교착상태가 발생할 수 있다.

따라서 notify를 활용한다면 어떠한 스레드에 적용할지 명시해주어야 한다(그렇지 않는다면 waiting pool의 임의의 스레드에만 통지).

6. Lock

동기화 키워드(synchronized) 사용 시 임계영역 설정을 같은 메서드 내에서만 할 수 있는데, 멀티스레드에서 이러한 임계영역을 공유할 수 있는 장치가 필요하다. 이것이 Lock이다.

6-1. ReentrantLock

public class MyClass {
    private Object lock1 = new Object();
    private Object lock2 = new Object();
    
    public void methodA() {
        synchronized (lock1) {
            methodB();
        }
    }
    
    public void methodB() {
        synchronized (lock2) {
            // do something
            methodA();
        }
    }
}

메서드A를 호출하면 lock1을 가진 상태에서 메서드B를 호출한다. 이때 메서드B는 lock2를 가진 상태에서 lock1이 풀릴때까지 기다리기에 서로 자원점유가 이루어지지 않는 데드락 상태가 발생할 수 있다.

이 상태에서 ReentrantLock을 사용하면 락을 가지더라도 실행할 수 있는 조건을 명시할 수 있다.

  • 재진입이 가능한 Lock, 배타 Lock의 일종이지만 Lock을 가지고 있더라도 실행이 가능한 상황을 만든다.
  • 특정 조건에서 Lock을 풀고, 나중에 다시 Lock을 얻어 임계영역으로 진입이 가능하다.

6-2. ReentrantReadWriteLock

Read Lock과 Write Lock을 별도로 제공하며, 읽기는 공유하고 쓰기는 배타Lock을 제공하는 Lock이다.

읽기 Lock은 중복 Lock을 설정할 수 있는 반면, 읽기 Lock이 걸려있는 상태에서는 쓰기 Lock을 설정할 수 없다(데이터 변경 방지).

6-3. StampedLock

ReentrantReadWriteLock에 낙관락 기능을 제공, 즉 데이터를 실제로 변경할 때만 lock을 건다. 쓰기가 빈번하지 않고 데이터 변경 시 충돌이 적을 경우 혹은 그 위험성이 없다고 판단하였을 경우에 빠른 변경처리를 위해 사용하는 lock의 일종이다.

  • 쓰기 작업이 발생했을 때 데이터가 이미 변경된 경우 다시 읽기 작업을 수행하여 새로운 값을 읽어들이고, 변경 작업을 다시 수행하도록 한다. 이러한 방식으로 쓰기 작업이 빈번하지 않은 경우에는 낙관적인 락을 사용하여 더 빠른 처리를 기대할 수 있다.
  • 낙관적인 읽기 Lock은 쓰기 Lock에 의해 바로 해제 가능하며, 서로 충돌할 경우에만 설정할 수 있다.

7. Condition

wait(), notify()에서 waiting pool에 있는 스레드를 구분할 수 없는 문제를 해결하기 위한 방안이 Condition이며, await()와 signal() 메서드를 사용한다.

따라서 특정 조건이 만족하였을 경우에만 Lock을 해제할 수 있고, ReentrantLock과 같이 활용할 수 있는 키워드이다.

(*condition1.await() or condition1.signal())

0개의 댓글

관련 채용 정보