스레드 안정성

공유되고 변경할 수 있는 상태에 대한 접근을 관리하는 것이다. 스레드 안정성은 코드를 보호하는 게 아니라, 데이터에 제어 없이 동시 접근하는 걸 막으려는 의미

스레드가 하나 이상 변수에 접근하고 + 변수에 값을 쓰면 해당 변수에 접근할 때 관련된 모든 스레드가 동기화를 통해 조율해야 한다.

어떤 스레드가 변경할 수 있는 상태 변수를 적절한 동기화 없이 접근한다면 그 프로그램은 잘못됐다. 이런 문제를 해결하는 3가지 방법이 있다.

변수를 스레드 간에 공유하지 않는다. (스택에 저장된 변수 접근 등)

변수를 변경할 수 없도록 만든다.

접근할 땐 언제나 동기화를 사용한다.

스레드 안전성이란?

여러 스레드가 클래스에 접근할 때 계속 정확하게 동작하면 해당 클래스는 안전하다. 스레드 안전한 클래스? : 클라이언트 쪽에서 별도로 동기화할 필요가 없도록 동기화 기능도 캡슐화 상태 없는 객체는 항상 스레드 안전하다.

단일 연산

경쟁 조건 (race condition)

타이밍이 딱 맞았을 때만 정답을 얻는 경우

경쟁 조건 형태

점검 후 행동 (check-then-act)

관찰 결과의 무효화, 즉 잠재적으로 유효하지 않은 관찰 결과로 결정을 내리거나 계산하는 것
e.g. 파일 X 가 없음을 확인하고 파일 X를 생성한다. 하지만 그 사이에 다른 누군가가 파일 X를 이미 생성했을 수 있다. 이런 경우 문제가 발생한다.

읽고 수정하고 쓰기 (read-modify-write)

이전 상태 기준으로 객체의 상태를 변경한다.
e.g. 부주의한 싱글톤 구현
class LazyInitRace {
  private var instance: LazyInitRace? = null
  
  fun getInstance(): LazyInitRace {
    if (instance == null) {
      // LazyInitRace 객체를 생성하기 전에 다른 Thread 에서도
      // 이 조건문을 타고 들어올 수 있다.
      instance = LazyInitRace()
    }
    return instance
  }
}
  • 없으면 추가하는 (put-if-absent)

  • if (!vector.contains(elem)) vector.add(elem)

  • 경쟁 조건을 피하려면 변수가 수정되는 동안 다른 스레드가 해당 변수를 사용하지 못하도록 막을 방법이 있어야 한다.

    • 다른 스레드는 수정 도중에 해당 변수에 접근할 수 없다. 즉, 수정 이전이나 이후에만 상태를 읽거나 변경할 수 있다는 뜻이다.

복합 동작 (compound action)

  • 스레드 안전성을 보장하기 위해서는 점검 후 행동 (check-then-act), 읽고 수정하고 쓰기 (read-modify-write) 동작이 항상 단인 연산이어야 한다.

  • 위와 같은 동작을 복합 동작이라고 한다.

  • 복합 동작 스레드에 안전하기 위해서는 전체가 단일 연산으로 실행돼야 하는 일련의 동작

락 (lock)

  • 여러 개의 변수가 하나의 불변조건을 구성하고 있다면, 이 변수들은 서로 독립적이지 않다.

  • 각 변수들 그 자체로 스레드 안전하다고 해서 전체 클래스의 상태가 안전하지는 않다는 뜻이다.

  • 🌟상태를 일관성 있게 유지하려면 관련 있는 변수들을 하나의 단일 연산으로 갱신해야 한다.

암묵적인 락

  • synchronized 구문

  • 자바에 내장된 락을 암묵적인 락(intrinsic lock) 혹은 모니터 락(monitor lock)이라고 한다.

  • 스레드가 synchronized 블록에 들어가기 전에 자동 확보

  • 정상적으로든 예외가 발생하든 해당 블록을 벗어날 때 자동으로 해제된다.

한 번에 한 스레드만 특정 락을 소유할 수 있다.

특정 락으로 보호된 코드 블록은 한 번에 한 스레드만 실행할 수 있다.

재진입성 (reentrant)

재진입성 👉 스레드 단위로 락을 얻는다 특정 스레드가 자기가 이미 획득한 락을 다시 확보할 수 있다.

락으로 상태 보호하기

  • 락은 자신이 보호하는 코드 경로에 여러 스레드가 순차적으로 접근하도록 한다.

  • 따라서 공유된 상태에 한 시점에 오직 한 스레드만 접근할 수 있도록 보장하는 규칙을 만들 때 유용하다.

  • 락을 활용하는 일반적인 사용 예는

    1. 모든 변경 가능한 변수를 객체 안에 캡슐화

    2. 암묵적인 락을 사용, 해당 변수에 접근하는 모든 코드 경로를 동기화함

    3. 객체 상태 나타내는 모든 변수는 객체의 암묵적인 락으로 보호

  • 🌟 여러 변수에 대한 불변조건이 있으면, 해당 변수들은 모두 같은 락으로 보호해야 한다.

활동성과 성능

synchronized 블록의 범위를 스레드 안정성을 유지할 정도로만 최소로 잡자.

  • 블록 안의 코드가 무엇을 하는지, 얼마나 걸릴지 파악해야한다.

  • 락을 오래 잡고 있으면 활동성이나 성능 문제를 야기한다.

🌟 복잡하고 오래 걸리는 계산, 네트웍 작업, 사용자 입출력 작업과 같이 오래 걸리는 부분은 가능한 락을 잡지 말자.

Last updated