스레드 안전성
- 스레드 안정성이 마치 코드를 보호하는 것처럼 이해하는 경우가 많지만, 실제로는 데이터에 제어없이 동시 접근하는 걸 막으려는 의미임을 알아두자.
- 객체가 스레드에 안전해야 하느냐는 해당 객체에 여러 스레드가 접근할지의 여부에 달렸다. 즉 프로그램에서 객체가 어떻게 사용되는가의 문제지 그 객체가 뭘 하느냐와는 무관하다.
- 객체를 스레드에 안전하게 만들려면 동기화를 통해 변경할 수 있는 상태에 접근하는 과정을 조율해야 한다.
- 스레드가 하나 이상 상태 변수에 접근하고 그 중 하나라도 변수에 값을 쓰면, 해당 변수에 접근할 때 관련된 모든 스레드가 동기화를 통해 조율해야 한다.
- 동기화 수단
- synchronized 키워드
- volatile 변수
- 명시적 락
- 단일 연산 변수 atomic variable
- 만약 여러 스레드가 변경할 수 있는 하나의 상태 변수를 적절한 동기화 없이 접근하면 그 프로그램은 잘못된 것이다. 이렇게 잘못된 프로그램을 고치는 데는 세 가지 방법이 있다.
- 해당 상태 변수를 스레드 간에 공유하지 않거나
- 해당 상태 변수를 변경할 수 없도록 만들거나
- 해당 상태 변수에 접근할 땐 언제나 동기화를 사용한다.
- 스레드 안전성을 확보하기 위해 나중에 클래스를 고치는 것보다 애당초 스레드에 안전하게 설계하는 편이 훨씬 쉽다.
- 객체 지향 프로그래밍 기법에서 사용하는 캡슐화나 데이터 은닉같은 기법이 스레드에 안전한 클래스를 작성하는 데도 도움이 될 수 있다.
<aside>
✅ 스레드 안전한 클래스를 설계할 땐, 바람직한 객체 지향 기법이 왕도다.
캡슐화와 불변 객체를 잘 활용하고, 불변 조건을 명확하게 기술해야 한다.
</aside>
경쟁 조건 Race Condition
- 경쟁 조건은 상대적인 시점이나 또는 JVM이 여러 스레드를 교차해서 실행하는 상황에 따라 계산의 정확성이 달라질 때 나타난다. 다시 말하자면 타이밍이 딱 맞았을 때만 정답을 얻는 경우를 말한다.
- check-then-act 형태의 구문
- 잠재적으로 유효하지 않은 값을 참조해서 다음에 뭘 할지를 결정하는 점검 후 행동 형태의 구문
- 원하는 결과를 얻을 수 있을지의 여부는 여러 가지 사건의 상대적인 시점에 따라 달라진다.
- 늦은 초기화 lazy initialization
- 스레드 안전성을 보장하기 위해서는 점검 후 행동과 읽고 수정하고 쓰기 등의 작업은 항상 단일 연산 이어야 한다.
- 복합 동작 Compound Action
- 점검 후 행동
- 읽고 수정하고 쓰기 같은 일련의 동작
<aside>
✅ AtomicLong처럼 스레드에 안전하게 이미 만들어져 있는 객체를 사용하는 편이 좋다. 스레드 안전하지 않은 상태 변수를 선언해두고 사용하는 것보다 이미 스레드 안전하게 만들어진 클래스가 가질 수 있는 가능한 상태의 변화를 파악하는 편이 훨씬 쉽고, 스레드 안전성을 더 쉽게 유지하고 검증할 수 있다.
</aside>
<aside>
✅ 상태를 일관성 있게 유지하려면 관련 있는 변수들을 하나의 단일 연산으로 갱신해야 한다.
</aside>
암묵적인 락
- 자바에서 암묵적인 락은 뮤텍스(mutexes) 또는 상호배제락(mutual exclusion lock)으로 동작한다.
- 즉 한 번에 한 스레드만 특정 락을 소유할 수 있다.
- 스레드 B가 가지고 있는 락을 스레드 A가 얻으려면, A는 B가 해당 락을 놓을 때까지 기다려야 한다.
객체 공유
- 메모리상의 공유된 변수를 여러 스레드에서 서로 사용할 수 있게 하려면 반드시 동기화 기능을 구현해야 한다.
- 여러 스레드에서 공동으로 사용하는 변수에는 항상 적절한 동기화 기법을 적용한다.
단일하지 않은 64비트 연산
- 자바 메모리 모델은 메모리에서 값을 가져오고 저장하는 연산이 단일해야 한다고 정의하고 있지만,
- volatile로 지정되지 않은 long이나 double 형의 64비트 값에 대해서는 메모리에 쓰거나 읽을 때 두 번의 32비트 연산을 사용할 수 있도록 허용하고 있다.
- 따라서 volatile을 지정하지 않은 long 변수의 값을 쓰는 기능과 읽는 기능이 서로 다른 스레드에서 동작한다면, 이전 값과 최신 값에서 각각 32비트를 읽어올 가능성이 생긴다.
- 즉 stale 문제를 신경쓰지 않는다 해도, volatile로 지정하지도 않고 락을 사용해 동기화하지도 않은 상태로 long이나 double 값을 동시에 여러 스레드에서 사용할 수 있다면 항상 이상한 문제를 만날 가능성이 있다.
<aside>
✅ 여러 스레드에서 사용하는 변수를 적당한 락으로 막아주지 않는다면, 스테일 상태에 쉽게 빠질 수 있다.
</aside>
락은 상호 배제(mutual exclusion)뿐만 아니라 정상적인 메모리 가시성을 확보하기 위해서도 사용한다.
변경 가능하면서 여러 스레드가 공유해 사용하는 변수를 각 스레드에서 각자 최신의 정상적인 값으로 활용하려면 동일한 락을 사용해 모두 동기화 시켜야 한다.