그러나 동시성은 성능과 코드 작성 측면에 부하를 유발하고, 복잡하고, 버그 재현이 어렵고, 시스템 구조가 크게 달라질 수 있다.
난관
여러 스레드가 같은 변수를 동시에 참조하면 race condition이 발생할 수 있다.
동시성 방어 원칙
SRP를 준수하여 동시성 관련 코드는 다른 코드와 분리해야 한다.
공유 객체를 사용하는 코드 내 critical section을 synchronized 키워드로 보호하라.
자료를 캡슐화하고 공유 자료를 최대한 줄여라.
객체를 복사해 읽기 전용으로 사용하라.
스레드는 가능한 공유 자료 없이 독립적으로 구현하라.
라이브러리를 이해하라
스레드 환경에 안전한 컬렉션을 사용하라.
서로 무관한 작업을 수행할 때는 executor 프레임워크를 사용하라.
가능하다면 스레드가 blocking 되지 않는 방법을 사용한다.
일부 클래스 라이브러리는 스레드에 안전하지 못하다.
java.util.concurrent, java.util.concurrent.atomic, java.util.concurrent.locks 이 라이브러리들을 이해하고 활용하여 스레드 환경에 안전한 코드를 구현하라.
실행 모델을 이해하라
생산자 소비자 : 하나 이상 생산자 스레드가 정보를 생성해 버퍼나 큐에 넣는다. 하나 이상의 소비자 스레드가 큐에서 정보를 가져와 사용한다. 큐에서 정보를 가져왔다는 시그널과 큐에 정보를 넣었다는 시그널을 주고 받으며 진행한다. 잘못하면 둘 다 가만히 시그널을 기다릴 수도 있다.
읽기 쓰기 : 읽기 쓰레드가 공유 자원을 읽고, 쓰기 쓰레드가 공유 자원을 갱신한다. 쓰기 스레드가 우선되면 처리율이 떨어지고, 읽기 스레드가 우선되면 기아 현상이 발생할 수 있다. 둘 사이에 균형이 중요하다.
식사하는 철학자들 : 데드락이 가능한 상황이다.
동기화하는 메서드 사이에 존재하는 의존성을 이해하라
공유 객체 하나에는 메서드 하나만 사용하라.
여러 메서드가 필요한 경우, 클라이언트에서 잠그거나, 서버에서 잠그거나, 잠금을 수행하는 중간 단계를 생성하는 것을 고려하라.
동기화하는 부분을 작게 만들어라
락은 스레드를 지연시키고 부하를 가중시킨다.
critical section의 수를 최대한 줄이고, 그 크기를 작게 만들어라
올바른 종료 코드는 구현하기 어렵다
스레드가 blocked 상태에서 종료 시그널을 받지 못하여 데드락이 발생할 수 있다.
종료 코드를 처음부터 고민해서 잘 동작하게 구현하라.
이는 쉽지 않기 대문에 이미 나온 알고리즘을 검토하라.
스레드 코드 테스트하기
문제를 노출하는 테스트 케이스를 작성하고, 프로그램 설정과 시스템 설정과 부하를 바꿔가며 자주 돌리고, 테스트 실패시에 원인을 추적하고, 다시 돌려서 통과하더라도 그냥 넘어가지마라.
말이 안되는 실패는 잠정적인 스레드 문제로 취급하라
대다수 개발자는 스레드가 다른 코드와 교류하는 방식을 직관적으로 이해하지 못한다.
이해하기 어려운 실패에 대해서 일회성 문제로 넘어가지 마라
다중 스레드를 고려하지 않은 순차 코드부터 제대로 돌게 만들자
스레드가 호출하는 POJO를 만들어서 스레드 환경 밖에서 테스트하여 로직 자체가 문제인지 확인하라
다중 스레드를 쓰는 코드 부분을 다양한 환경에 쉽게 끼워 넣을 수 있게 스레드 코드를 구현하라
다양한 설정으로 실행하기 쉽게 구현하여 실행 중 스레드 수를 바꿔보고, 다양한 속도로 실행해보고, 반복 테스트가 가능하도록 테스트 케이스를 작성하라
다중 스레드를 쓰는 코드 부분을 상황에 맞게 조율할 수 있게 작성하라
적절한 스레드 개수를 파악하려면 시행착오가 필요하기 때문에 스레드 개수를 조율하기 쉽게 코드를 구현하라
프로그램 상황에 따라 스스로 스레드 개수를 조정하는 코드도 고민하라
프로세서 수보다 많은 스레드를 돌려보라
시스템이 스레드를 스와핑할 때도 문제가 발생하기 때문에 스와핑이 잦을수록 critical section 누락이나 데드락을 찾기 쉬워진다.
다른 플랫폼에서 돌려보라
운영체제마다 스레드를 처리하는 정책이 달라 결과가 달라질 수 있다.
처음부터 그리고 자주 사용할 가능성이 있는 플랫폼 전부에서 테스트를 수행하라
코드에 보조 코드를 넣어 돌려라. 강제로 실패를 일으키게 해보라
스레드 버그는 코드가 실행되는 수천가지 경로 중에 아주 소수만 실패하기 때문에 산반적이고 우발적이고 재현이 어렵다.
wait(), sleep(), yield(), priority() 같은 메서드를 추가해 코드를 다양한 순서로 실행하라
직접 보조 코드를 넣기에는 적정 위치를 찾기 어렵고 배포 환경에 보조 코드가 남아 있으면 성능에 악영향을 미칠 수 있다.
무작위로 sleep이나 yield를 호출하거나 아무 동작도 하지 않는 jiggle 메서드를 구현하여 스레드를 매번 다른 순서로 실행되도록 하면 스레드 오류가 드러날 확률을 높일 수 있다.