1.1. 개요
스레드 안전성 수준을 명시해야하는 이유와, 스레드 안전성 수준에 대해 알아보자.
1.2. 멀티 스레드 환경에서의 메서드 호출
멀티 스레드 환경에서 메서드를 호출했을 때, 메서드 내에서 사용하는 변수 및 인스턴스들의 동기화가 필요한지 알아야한다. 이에 따라 클라이언트에서 이 메서드를 사용하는 방식이 달라지기 때문이다.
여러 스레드가 동시에 호출할 때 메서드가 어떻게 동작하느냐는 해당 클래스와 이를 사용하는 클라이언트 사이의 중요한 계약 사항 중 하나라고 할 수 있다.
만약 이러한 내용이 없을 경우 클라이언트는 나름의 가정과 함께 사용할것인데, 이 과정에서 심각한 오류를 발생시킬 수 있다.
1.3. synchronized == 스레드 안전하다?
API 문서에 synchronized 한정자가 붙어 있다고 모두 스레드 안전하다고 할 수 없다. 한정자를 스레드 안전한 API 동작을 목적으로 사용했을 수도 있지만 단순 구현 이슈로 인해 사용했을 수도 있기 때문이다. "synchronized == 멀티 스레드 안전" 이라고 단정짓는 것은 위험하다.
1.4. 스레드 안전에 대한 명시
어떤 메서드를 구현했을 때 스레드 안전성 여부를 명시해야한다. 하지만 내부 구현에 따라 멀티 스레드 환경에서 무조건 안전할수도, 조건부 안전할수도, 클라이언트가 동기화 메커니즘을 적용했을 때 안전할 수도, 무슨 짓을 해도 안전하지 않을수도 있다. 결국 안전성 수준을 명시해야 한다.
2. 스레드 안전성 수준
2.1. 불변(immutable) - @Immutable
클래스의 인스턴스가 마치 상수와 같아서 외부 동기화가 필요없는 수준이다. 대표적으로 String, Long이 있다.
2.2. 무조건적 스레드 안전(unconditionally thread-safe) - @ThreadSafe
클래스의 인스턴스는 수정될 수 있으나, 내부에서 충실히 동기화하여 별도의 외부 동기화가 없이 동시에 사용해도 안전한 수준이다. 대표적으로 AtomicLong, ConcurrentHashMap이 있다.
* ConcurrentHashMap
HashMap에서 세분화된 락을 제공하는 Map 자료형을 말한다. 전체 맵을 잠그는 대신, 내부 데이터를 여러 버킷으로 나누고 개별적인 락을 적용한다. 이를 통해 여러 스레드에서 동시에 다른 버킷에 접근 가능하다.
읽기 작업은 락 없이 수행하며, compute(), merge()와 같은 원자적 연산도 제공한다.
이를 잘 활용하면 멀티 스레드에서 정합성을 유지할 수 있다.
2.3. 조건부 스레드 안전(conditionally thread-safe) - @ThreadSafe
무조건적 스레드 안전과 같으나, 일부 메서드는 외부 동기화가 필요한 수준이다. Collections.synchronized 래퍼 메서드가 반환한 컬렉션들이 여기 속한다.
* 조건부 스레드 문서화 시에는 어떤 순서로 호출할 때 외부 동기화가 필요한지, 어떤 락을 얻어야 하는지를 알려줘야한다. 예를들어 Collections.synchronizedMap의 API 문서에는 다음과 같이 써 있다.
synchronizedMap이 반환한 맵의 컬렉션 뷰를 순회하려면 반드시 그 맵을 락으로 사용해 수동으로 동기화하라.
Map<K, V> m= Collections.synchronizedMap(new HashMap<>());
Set<K> s = m.keySet();
synchronized (m){ // s가 아닌 m을 사용해 동기화해야한다
for (K key : s){
key.f();
}
}
2.4. 스레드 안전하지 않음(not thread-safe) - @NotThreadSafe
클래스의 인스턴스는 수정될 수 있다. 하지만 멀티 쓰레드 환경에서 사용하려면 클라이언트가 외부 동기화 메커니즘을 사용해야한다. 대표적으로 ArrayList, HashMap이 있다.
2.5. 스레드 적대적(thread-hostile)
외부 동기화로 감싸더라도 안전하지 않은 수준을 말한다.
3. 정리
클래스의 스레드 안전성은 보통 클래스의 문서화 주석에 기재하지만, Collections.synchronizedMap과 같이 조건을 명시해야할 경우엔 코드 레벨의 주석에 기재하는 것이 좋다.
추가로 클래스가 외부에서 사용할 수 있는 락을 제공하면 클라이언트에서 일련의 메서드 호출을 원자적으로 수행할 수 있다.
'공부 > Effective Java' 카테고리의 다른 글
[Effective Java] readObject 메서드는 방어적으로 작성하라 / 역직렬화 / readObject (0) | 2024.11.07 |
---|---|
[Effective Java] Item 84. 프로그램의 동작을 스레드 스케줄러에 기대지 말라 (0) | 2024.10.10 |
[Effective Java] Item 73. 추상화 수준에 맞는 예외를 던져라 (1) | 2024.08.22 |
[Effective Java] Item 72. 표준 예외를 사용하라 (0) | 2024.08.15 |
[Effective Java] Item 70. 복구할 수 있는 상황에는 검사 예외를, 프로그래밍 오류에는 런타임 예외를 사용하라 (0) | 2024.07.04 |