한 메서드를 여러 스레드가 동시에 호출할 때 그 메서드가 어떻게 동작하느냐는 해당 클래스와 이를 사용하는 클라이언트 사이의 중요한 계약과 같으므로 API 문서에 작성해야한다.
메서드 선언에 synchronized 한정자를 선언할지는 구현 이슈일 뿐 API에 속하지 않으므로 이 메서드가 synchronized 한정자를 가진다고 해서 스레드 안전하다고 믿기 어렵다.
멀티스레드 환경에서도 API를 안전하게 사용하게 하려면 클래스가 지원하는 스레드 안전성 수준을 정확히 명시해야한다.
/**
* Returns a synchronized (thread-safe) map backed by the specified
* map. In order to guarantee serial access, it is critical that
* <strong>all</strong> access to the backing map is accomplished
* through the returned map.<p>
*
* It is imperative that the user manually synchronize on the returned
* map when traversing any of its collection views via {@link Iterator},
* {@link Spliterator} or {@link Stream}:
* <pre>
* Map m = Collections.synchronizedMap(new HashMap());
* ...
* Set s = m.keySet(); // Needn't be in synchronized block
* ...
* synchronized (m) { // Synchronizing on m, not s!
* Iterator i = s.iterator(); // Must be in synchronized block
* while (i.hasNext())
* foo(i.next());
* }
* </pre>
* Failure to follow this advice may result in non-deterministic behavior.
*
* <p>The returned map will be serializable if the specified map is
* serializable.
*
* @param <K> the class of the map keys
* @param <V> the class of the map values
* @param m the map to be "wrapped" in a synchronized map.
* @return a synchronized view of the specified map.
*/
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
return new SynchronizedMap<>(m);
}
위는 synchronizedMap의 일부이다. 주석 중 다음의 부분에 주목해보자. * It is imperative that the user manually synchronize on the returned
* map when traversing any of its collection views via {@link Iterator},
* {@link Spliterator} or {@link Stream}:
* <pre>
* Map m = Collections.synchronizedMap(new HashMap());
* ...
* Set s = m.keySet(); // Needn't be in synchronized block
* ...
* synchronized (m) { // Synchronizing on m, not s!
* Iterator i = s.iterator(); // Must be in synchronized block
* while (i.hasNext())
* foo(i.next());
* }
* </pre>
* Failure to follow this advice may result in non-deterministic behavior.
마지막 줄에서 위를 따르지 않으면 행동을 예측할 수 없는 결과를 얻을 것이라고 적혀있다. 클래스가 외부에서 사용할 수 있는 락을 제공하면 메서드 호출을 원자적으로 수행할 수 있으나, 이 유연성엔 대가가 따른다. 내부에서 처리하는 고성능 동시성 제어 메커니즘과 혼용할 수 없게 되어 동시성 컬렉션과는 함께 사용하지 못한다. 또한 클라이언트가 공개된 락을 오래 쥐고 놓지 않는 서비스 거부 공격(DOS)을 수행할 수도 있다.
이 공격을 막으려면 synchronized 메서드 대신 비공개 락 객체를 사용해야한다.
private final Object lock = new Object();
public void foo() {
synchronized(lock) {
...
}
}
비공개 락 객체를 사용하면 클라이언트가 동기화에 관여할 수 없다. 또한 락 필드는 항상 final로 선언하자. 또한, 무조건적 스레드 안전 클래스에서만 사용할 수 있다.
이펙티브 자바 3/E