## 목차 ##



## 동시성이 필요한 이유? ##

  • 동시성은 '무엇'과 '언제'를 분리한다.

    • 단일 스레드에서는 '무엇'과 '언제'가 밀접하다.
    • 다중 스레드 세상에서는 프로그램이 작은 협력 프로그램 여러개로 보인다.
  • 응답시간 및 작업 처리량(throughput) 개선을 위해서

    • 여러 작업을 동시에 처리하므로 응답속도가 빨라진다.
  • 동시성은 항상 성능을 높여준다?

    • 여러 프로세서가 동시에 처리할 독립적인 계산과정이 충분히 많은 경우에는 그렇다.
  • 동시성을 구현해도 설계는 변하지 않는다?

    • 단일 스레드 시스템과 멀티 스레드 시스템은 설계가 판이하게 다르다.
  • 동시성은 부하를 유발한다.

    • 성능 측면에서 부하가 걸린다.
  • 간단한 문제라도 동시성은 복잡하다.

  • 동시성에서 기인하는 버그는 재현하기 어렵다.

    • 일회성 문제라 여기고 무시하고 넘어가기 쉽다.



## 난관 ##

동시성을 구현하기 어려운 이유. 아래 코드를 보자.

public class X {
    private int lastIdUsed;

    public int getNextId() {
        return ++lastIdUsed;
    }
}

인스턴스 X를 생성하면서 lastIdUsed를 42로 초기화했다. thread1, thread2 두 스레드가 해당 객체의 getNextId()를 호출하면 결과는 셋 중 하나다.

(a) thread1은 43을, thread2는 44를 받는다. lastIdUsed는 44가 된다.
(b) thread1은 44를, thread2는 43을 받는다. lastIdUsed는 44가 된다.
(c) thread1과 thread2가 모두 43을 받는다. lastIdUsed는 그대로 43이다.

(c)와같은 놀라운 결과가 발생할 수 있다. 두 스레드가 자바 코드 한 줄을 거쳐가는 경로는 수없이 많다. 이것을 이해하려면 JIT 컴파일러와 자바 메모리모델이 원자로 간주하는 최소단위를 알아야한다.

대부분의 경로는 (a)나 (b)와 같은 올바른 경로를 내놓지만 문제는 잘못된 결과를 내놓는 '일부' 경로다.



## 동시성 방어 원칙 ##

  • 단일 책임 원칙(SRP)

    • 동시성 코드는 복잡성 하나만으로 따로 분리될 이유가 충분하다.
    • 동시성 관련 코드는 다른 코드에서 분리해야한다.
  • 자료 범위를 제한하라

    • 공유객체를 사용하는 임계영역(critical section) 코드를 synchronized 키워드로 보호한다.
    • 임계영역의 수를 최소로 줄여라.
    • 공유자료를 수정하는 위치가 많을수록 보호를 빼먹게되고 디버깅이 더욱 어려워진다.
    • 공유자료를 캡슐화하한다.
  • 자료 사본을 사용하라

    • 객체를 복사해서 사용한다.
    • 객체 복사에 드는 시간과 부하를 고려해 복사 비용을 실측해보고 결정한다.
    • 객체를 복사함으로써 데드락을 피하는 이점이 사본 생성과 가비지 컬렉션 오버헤드 손실을 상쇄할 가능성이 크다.
  • 스레드는 가능한 독립적으로 구현하라

    • 가능하면 다른 스레드와 자료를 공유하지 않는다.



## 라이브러리를 이해하라 ##

  • 스레드 환경에 안전한 컬렉션을 사용한다.

    • java.util.concurrent 패키지
    • ConcurrentHashMap은 Thread-safe하며 거의 모든 상황에서 HashMap보다 빠르다.
  • 서로 무관한 작업을 수행할 때는 executor 프레임워크를 사용한다.



## 실행 모델을 이해하라 ##

  • 생산자-소비자(Producer-Consumer) 모델

    • 하나 이상의 생산자가 데이터를 생성해 buffer, queue와 같은 자료구조에 넣는다.
    • 하나 이상의 소비자 스레드가 정보를 가져와 사용한다.
    • 생산자 스레드가 정보를 채우면 신호를 보내 데이터가 채워졌음을 알린다.
    • 소비자 스레드가 정보를 가져가면 신호를 보내 빈 공간이 있음을 알린다.
    • 잘못하면 진행 가능함에도 불구하고 서로의 시그널을 기다릴 가능성이 있다.
  • 읽기-쓰기

    • 공유된 자원을 읽는 쓰레드와 쓰는 쓰레드가 동시에 존재한다.
    • 공유자원이 이따금 갱신이 된다면 읽기/쓰기 스레드는 서로가 사용중일 때 기다려야한다.
    • 한 쪽에 우선순위를 주기에는 다른 한 쪽이 기아상태에 빠질 가능성이 있다.
    • 양쪽 균형을 잡는 해법이 필요하다.



## 동기화하는 메소드 사이에 존재하는 의존성을 이해하라 ##

  • 웬만하면 공유객체를 여러 synchronized 메소드로 보호하지 말자.
    • 불필요한 의존성이 생길 수 있다.
    • 웬만하면 하나로 줄이자.



## 동기화하는 부분을 작게 만들어라 ##

  • synchronized 키워드에 의한 락(lock)은 스레드를 지연시키고 부하를 가중시킨다.
    • synchronized 키워드를 남발하지 말자.
    • 임계영역은 반드시 보호해야한다.
    • 따라서 임계영역 수를 최대한 줄여야한다.



## 올바른 종료 코드는 구현하기 어렵다 ##

  • 데드락에 걸려 영원히 기다리는 현상 시 제대로 종료할 수 없게된다.
    • 개발 초기부터 이를 고려해 개발한다.
    • 어려우므로 이미 나온 알고리즘 사용을 검토한다.



## 스레드 코드 테스트하기 ##

  • 문제를 노출하는 테스트 케이스를 작성하라.

    • 여러가지 설정 및 부하를 바꿔가며 실행해봐라.
    • 실패했던 테스트가 다시 돌렸더니 통과하더라는 이유로 넘어가지 마라.
  • 말이 안 되는 실패는 잠정적인 스레드 문제로 취급하라

    • 멀티 스레드는 때때로 '말이 안 되는' 오류를 일으킨다.
    • 스레드 코드에 잠입한 버그는 수천, 수만번에 한 번씩 드러나기도 한다.
    • 실패를 재현하기 아주 어렵다.
    • 일회성 문제는 존재하지 않는다.
  • 멀티 스레드를 고려하지 않은 순차 코드부터 제대로 돌게 만들자.

    • 스레드 환경과 스레드 환경 밖에서 생기는 버그를 동시에 디버깅하지마라.
    • 순차 코드부터 제대로 돌리자.
  • 상황에 맞게 스레드를 제어할 수 있도록 코드를 작성하라.

    • 프로그램 처리율과 효율에 따라 스레드 수를 조율하는 코드
  • 프로세서 수보다 많은 스레드를 돌려보라.

    • 스레드 스와핑이 잦으면 임계영역을 빼먹은 코드나 데드락을 일으키는 코드를 찾기 쉬워진다.
  • 보조 코드를 활용해라.

    • 스레드 버그를 재현하기 어려운 이유는 수천, 수만가지 코드실행 경로중에 극소수만 실패하기 때문이다.
    • sleep(), wait(), yield(), priority()와 같이 실행경로에 영향을 미치는 메소드를 추가해 실행해본다.
    • AOF, CGLIB, ASM등과 같은 도구를 사용하면 보조코드를 자동으로 추가할 수 있다.



## 결론 ##

  • 멀티 스레딩은 매우 어렵다.

  • 주의하지 않으면 매우 이해하기 어려운 버그를 만날 수 있다.

  • SRP를 준수해라.

  • 동시성 오류를 일으키는 원인들에 대해 이해한다.

    • 공유 자료 조작, 자원 풀 공유 등
  • 코드 보호

    • 임계영역 코드를 보호해라.(synchronized 키워드)
    • 잠글 필요가 없는곳을 절대 잠그지 않는다.
    • 잠긴 영역에서 다른 잠긴 영역을 호출하지 않는다.
    • 공유하는 객체 수와 범위를 최대한 줄인다.
  • 스레드에서는 알 수 없는 일회성 버그들이 많다.

    • 많은 플랫폼에서 많은 설정 방식을 통해 계속해서 반복테스트 해야한다.
  • 보조코드를 추가하면 버그가 드러날 가능성이 높아진다.

    • 직접 구현, 자동화 기술 사용

+ Recent posts