선생님, 개발을 잘하고 싶어요.

자바 병렬 프로그래밍 - 1부 3장 - 객체 공유 본문

일상/책 리뷰

자바 병렬 프로그래밍 - 1부 3장 - 객체 공유

알고싶은 승민 2022. 6. 10. 21:59
  • 여러 개의 스레드에서 객체를 동시에 사용하려 할 때 섞이지 않고 안전하게 동작하도록
  • 객체를 공유하고 공개하는 방법을 살핀다.
  • 크리티컬 섹션?
  • 코드 블록을 동기화할 때 항상 메모리 가시성(Memory Visibility) 문제가 발생한다.
  • 반드시 달성돼야 하는 거
    • 변수를 사용하고 있을 때 다른 스레드가 해당 변수 값을 사용하지 못하도록 막아야한다.
    • 동기화 블록을 빠져나가고 나면 다른 스레드가 변경된 값을 즉시 사용할 수 있어야한다.

가시성

  • 변수에 값을 저장하고 이후에 값을 다시 읽으면, 아까 저장한 값을 가져올 수 있을거라고 예상한다.
  • 하지만 멀티 스레드 환경에서는
    • 특정 변수의 값을 가져갈 때 다른 스레드가 작성한 값을 가져갈 수 없을 수 있다.
    • 심지어 값을 읽지 못할 수 있다.
  • 🌟 메모리상 공유된 변수를 여러 스레드에서 서로 사용할 수 있게 하려면 반드시 동기화 기능을 구현해야 한다.
  • 재배치(reordering): 특정 메소드의 소스코드가 100% 코딩된 순서로 동작함을 보장할 수 없다.
    • 동기화를 지정하지 않으면 컴파일러나 프로세서, JVM이 프로그램 코드 실행 순서를 임의로 바꾸는 경우가 있다.
  • 🌟 여러 스레드에서 공동으로 사용하는 변수에는 항상 적절한 동기화 기법을 적용한다
  • 스레드는 메모리를 공유하는데, 컴파일, 런타임 최적화 시점에 메모리 이외의 저장소를 활용할 수 있는게 원인인가? 프로세서 레지스터나 외부 캐시 등

스테일 데이터 (stale data)

  • 변수를 사용하는 모든 경우에 동기화를 시켜두지 않으면 해당 변수에 대한 최신 값이 아닌 다른 값을 사용하게 되는 경우가 발생할 수 있다.
    • 다른 스레드에서 작성한 값을 가져갈 수 없기 때문에, 변수를 사용하려고 접근하기 전에 다른 스레드가 값을 갱신했다면, 이미 최신 값이 아닌 값으로 작업을 진행할 수 도 있다.

단일하지 않은 64비트 연산

  • 숫자형에 volatile 키워드를 사용하지 않으면 난데없는 값이 들어갈 수도 있다.
  • 64비트 값에 대해 메모리에 쓰거나 읽을 때 두 번의 32비트 연산을 할 수도 있기 때문…

락과 가시성

  • synchronized로 둘러싸인 코드에서 스레드 A가 사용한 모든 변수 값은, 같은 락을 사용하는 스레드 B가 실행할 때 안전하게 사용할 수 있다.
  • 🌟 락은 상호 배제뿐 아니라 정상적인 메모리 가시성을 확보하기 위해서도 사용한다.

volatile 변수

  • volatile 변수는 값을 바꿨을 때 다른 스레드에서 항상 최신 값을 읽어갈 수 있도록 해준다.
    • “이 변수는 공유해 사용하고, 실행 순서를 재배치 하지마”라고 컴파일러와 런타임에게 알린다.
  • 프로세서의 레지스터에 캐시되지도 않고, 프로세서 외부의 캐시에도 들어가지 않기 때문에 항상 다른 스레드가 보관해둔 최신의 값을 읽어갈 수 있다.
  • 락이나 동기화 기능과는 다르다. volatile 지정은 락이나 동기화 기능이 동작하시키지 않는다.
  • 메모리 가시성 입장에선 volatile이나 synchronized는 비슷한 효과를 가져온다.
  • 🌟 락은 가시성 + 연산의 단일성 보장, volatile은 가시성만 보장
  • 🌟 다음과 같은 경우만 사용하자.
    • 변수에 값을 저장하는 작업이 변수의 지금 값과 관련이 없는 경우
      • 연산의 단일성도 보장해야함.
    • 해당 변수의 값을 변경하는 스레드가 하나만 존재하는 경우
      • 변수 값 변경이 단일 스레드에서 진행되므로 동시성이 보장될 필요 없음.
    • 변수가 불변조건에 관련되어 있지 않은 경우
      • 동시성 보장이 필요없음
    • 변수를 사용하는 동안 어떤 경우라도 락을 걸어 둘 필요가 없는 경우 (?)

공개와 유출

  • 공개(published) 되었다 👉 어떤 객체를 현재 코드의 스코프 범위 밖에서 사용할 수 있도록 만듬
    • 이 경우에는 반드시 해당 객체를 동기화시켜야 한다.
  • 유출 상태(escaped) 👉 의도적으로 공개하지 않았지만, 외부에서 사용할 수 있게 공개된 경우
    • 특정 객체를 공개 하면서, 그와 관련된 다른 객체까지 덩달아 공개하는 경우
    • 내부 클래스(inner 클래스)의 인스턴스를 외부에 공개하는 경우
      • 내부 클래스 인스턴스는 항상 outer class의 참조를 가지고 있다.
    • 생성 메소드 실행 도중에 this 변수가 외부에 공개되는 경우도 있다.
  • 어떤 객체건 일단 유출되고 나면, 다른 스레드가 유출된 클래스를 잘못 사용할 수 있다고 가정해야함.
  • 🌟 생성 메소드 실행 도중에 this가 외부에 유출되면 안된다.
    • this를 넘겨서 이벤트 등록하는 이벤트를 생각할 수 있는데 이벤트 등록을 생성자에서 하는게 아니라 팩토리 메서드를 만들어 사용할 수 있다.
    // 생성자로 source를 받고 registerListener 코드를 등록하면
    // 무명 객체에 의해 부주의하게 this 참조가 유출되게 된다.
    class SafeListener private constructor() {
      val listener: EventListener = object : EventListener { .. }
    
      companion object {
        fun newInstance(source: EventSource): SafeListener {
          safe = SafeListener()
          source.registerListener(safe.listener)
          return safe
        }
      }  
    }
    

스레드 한정

  • 모든 변경가능한 객체를 다룰 때 선택할 수 있는 전략은 두 가지다.
    1. 스레드간 공유하는 경우는 동기화
    2. 스레드간 공유하지 않음
  • 2번 처럼, 객체를 사용하는 스레드를 한정(confine)하는 방법으로 스레드 안정성을 확보할 수 있다.
    • GUI 프로그래밍에선 이벤트 스레드를 제외한 다른 스레드에서 UI 객체를 사용할 수 없다.
  • 스레드 한정 기법은 프로그램 처음 설계부터 다뤄야한다.
  • 프로그램 구현 내내 한정 기법을 계속 적용해야한다.
  • 이러한 기법을 사용해도, 개발자는 스레드에 한정된 객체가 외부로 유출되지 않도록 신경 써야 한다.

스레드 한정 - 주먹구구식

  • GUI 모듈과 같은 특정 시스템을 단일 스레드로 동작하도록 만들 것이냐?
  • 특정 스레드에 한정하려는 객체가 volatile로 선언되어 있다면?
    • volatile 변수는 단일 스레드에서만 쓰기 작업을 한다면, 읽기 작업은 멀티 스레드에서 접근해도 안전하다. (가장 최근에 업데이트된 값을 정확하게 읽어갈 수 있다.)

스택 한정

  • 스택 한정 기법 👉 객체를 로컬 변수를 통해서만 사용할 수 있는 특별한 경우의 스레드 한정 기법
  • 로컬 변수는 모두 암묵적으로 현재 실행 중인 스레드에 한정되어 있다고 볼 수 있다. (스레드마다 스택을 가지므로 스택에 저장되는 변수는 스레드에 한정될 것)
  • reference 변수를 스택에 저장한다면, 그 객체에 대한 참조가 외부로 유출되지 않도록 개발자가 주의해야 한다. (객체의 참조는 힙, 즉 스레드 간 공유 영역에 존재하므로)
  • 스레드에 안전하지 않은 객체라도 특정 스레드 내부에서만 사용한다면 동기화 문제는 없다. 안전하다.

ThreadLocal

  • 호출하는 스레드마다 다른 값을 사용할 수 있도록 관리
  • ThreadLocal의 get메서드를 호출하면, 현재 실행 중인 스레드에서 최근에 set된 값을 가져올 수 있다.
  • e.g. 버퍼 처럼 임시로 사용할 객체를 매번 새로 생성하는 대신, 이미 만들어진 객체를 재활용하고자 할 때 많이 쓰인다.
  • 개념적으로 Map<Thread, T> 타입으로 생각할 수 있다.
  • 전역 변수가 아니면서도 전역 변수처럼 동작하므로 오용에 주의하자.

불변성 (immutablity)

  • 객체의 상태가 변하지 않는다면? 지금까지 봤던 복잡하고도 다양한 문제가 일순에 사라진다!
  • 🌟 불변 객체는 언제라도 스레드에 안전하다.
  • 객체 불변과 참조 불변은 구분해서 생각하자. 데이터가 불변 객체에 들어있다고 해도, final이 아니면 다른 불변 객체로 참조를 바꿀 수 있다. 프로그램의 데이터가 언제든지 바뀌는 셈

final 변수

  • 변수의 값을 변경할 수 없다.
  • 초기화 안정성을 보장한다. 별다른 동기화 작업 없이도 불변 객체를 자유롭게 사용하고 공유할 수 있다.

불변 객체를 공개할 때 volatile 키워드 사용

  • 경쟁 조건을 만드는 여러 변수를 모두 모아 하나의 불변 객체로 관리하면? 경쟁 조건을 방지할 수 있다.

안전 공개

  • 특정 데이터를 여러 개의 스레드에서 사용하도록 공유할 때 적절한 동기화 방법을 적용하지 않는다면 굉장히 이상한 일이 발생할 가능성이 높다.

불변 객체와 초기화 안정성

  • 🌟 불변 객체는 별다른 동기화 적용하지 않아도, 어느 스레드건 마음껏 안전하게 사용할 수 있다.

안전한 공개 방법의 특성

  • 불변 객체가 아닌 객체는 올바른 방법으로 안전하게 공개해야 한다.
  • 공개하는 스레드, 사용하는 스레드 양쪽 모두에 동기화 방법을 적용
  • 안전하게 공개? 👉 객체 참조 및 내부의 상태를 외부 스레드도 동시에 볼 수 있어야한다.
    • 객체 참조를 static 메소드에서 초기화
    • 객체 참조를 volatile or AtomicReference 클래스에 보관
    • 객체 참조를 올바르게 생성된 클래스 내부 final 변수에 보관
    • 락을 사용해 올바르게 막혀 있는 변수에 객체 참조 보관 (스레드 세이프한 API)
  • 스레드 동기화 기능 갖고 있는 API
    • Hashtable, ConcurrentMap ... 스레드 안전한 맵, 키 값
    • Vector, CopyOnWriteArrayList, CopyOnWriteArraySet … 스레드 안전한 컬렉션
    • BlokcingQueue, ConcurrentLinkedQueue …

결과적으로 불변인 객체

  • 불변 객체를 다시 찬양하자면….
    • 특정 객체를 안전한 방법으로 공개했을 경우
    • 객체에 대한 참조를 갖고 객체를 불러와 사용하는 시점에는 공개하는 시점의 객체 상태를 정확하게 사용할 수 있고,
    • 값이 바뀌지 않는 한 여러 스레드에서 동시에 값을 가져다 사용해도 동기화 문제가 없다.
  • 불변 객체인 것처럼 사용하면 동기화 작업을 하지 않아도 된다.

가변 객체

  • mutable object를 사용할 때는 공개하는 부분, 가변 객체를 사용하는 모든 부분에 동기화 코드를 작성해야한다.
  • 🌟 가변성에 따라 객체를 공개할 때 필요한 점
    • 불변 객체 → 어떤 방법으로 공개해도 문제 없음
    • 결과적 불변 객체 → 안전하게 공개하자
    • 가변 객체 → 안전하게 공개 & 스레드 안전하게 만들거나 & 락으로 동기화 필요.

객체를 안전하게 공유하기

  • 객체를 사용하기 전에 고려할 거
    • 동기화 코드를 적용해 락을 확보해야 하나?
    • 객체 내부 값을 바꿔도 괜찮나?
    • 값을 읽기만 해야하나?
  • 객체를 공개할 때
    • 해당 객체를 어떤 방법으로 사용할 수 있고, 사용해야 하는지에 대해 정확한 설명 필요

🌟 병렬 프로그램에서 객체 공유 원칙

스레드 한정

  • 스레드 한정 객체는 완전하게 해당 스레드 내부에만 존재 및 그 스레드만 호출 사용 가능

읽기 전용 객체 공유

  • 불변 객체와 결과적 불변 객체를 포함한다.
  • 별다른 동기화 작업 없이도 여러 스레드에서 마음껏 사용가능

스레드 안전 객체 공유

  • 객체 내부적으로 필수 동기화 기능이 있으니 외부에서 신경쓸 필요 없다. 여러 스레드에서 마음껏 사용

동기화 방법 적용

  • 객체 접근을 위해 락 획득을 기다린다.
Comments