여러 연산을 조합하여 사용시 스레드 안전성 문제

public static Object getLast(Vector list) {
    int lastIndex = list.size() - 1;
    return list.get(lastIndex);
}
  
public static void deleteLast(Vector list) {
    int lastIndex = list.size() - 1;
    list.remove(lastIndex);
}

위 코드는 스레드에 안전하지 못하다. 만약 delete연산과 getLast 연산 사이에 스레드 전환이 생겼을 경우 ArrayIndexOutOfBoundsException 이 발생할 수 있다.

따라서 이 코드는 다음과 같이 바뀌어야한다.

public static Object getLast(Vector list) {
    syncronized(list) {                 // 안전성 확보
        int lastIndex = list.size() - 1;
        return list.get(lastIndex);
    }
}
  
public static void deleteLast(Vector list) {
    syncronized(list) {                 // 안전성 확보
        int lastIndex = list.size() - 1;
        list.remove(lastIndex);
    }
}

동기화된 컬렉션 클래스는 대부분 클라이언트 측 락을 사용할 수 있도록 만들어져 있기 때문에, 이를 사용하여 새로운 기능을 만드는 클래스는 동일한 수준의 동기화 수단을 적용해야 한다.

Iterator와 ConcurrentModificationException

동기화된 컬렉션 역시 다중 연산을 사용할 때 발생하는 문제점을 해결하지는 못한다.

Iterator를 사용해 컬렉션을 읽는다 해도 반복문이 실행되는 동안 다른 스레드가 컬렉션에 내용을 추가/제거하는 변경 작업을 시도할 때 발생할 수 있는 문제를 막아주지는 못한다.

동기화된 컬렉션의 Iterator를 사용한다 해도 다른 스레드가 같은 시점 컬렉션을 변경하는 작업을 처리하지 못하게 만들어져 있고 대신 즉시 멈춤(fail-fast)의 형태로 구현되어 있다.

List<String> list = Collections.syncronizedList(...);
  
for (String item : list) {
    doSomething(item);          // 이런 작업은 좋지 않다
}

위 코드에서 doSomething 작업에서 다른 Lock 을 확보해야 하는 상황이 일어날 경우 데드락이 발생할 가능성도 높아진다. 검증된 doSomething 에게 작업을 위임해야한다.

순회 중 다른 스레드에 의한 변경을 막기위해 다음과 같이 전체를 동기화 하는 것은 좋지 않다.

List<String> list = Collections.syncronizedList(...);
  
syncronized(list) {
    for (String item : list) {
        // ...
    }
}

왜냐하면, list 내용이 굉장히 많다면 동시성이 매우 비효율적이기 때문이다. 반복문에서 락을 오래 잡고 있으면 스레드 대기 상태가 많아지고 대기 스레드가 적체될수록 성능이 나빠지기 때문이다.

컬렉션 자체를 clone 하는 방법도 있지만, 컬랙션이 클 경우 clone 에 시간이 오래걸릴 수 있다. 따라서 작업에 걸리는 시간 / 복사본 생성 비용 / 응답성 등을 고려해서 적용을 해야한다.