멀티스레드와 동시성

Ch11. 동시성 컬렉션 - Synchronized

webmaster 2024. 9. 15. 15:41
728x90

자바가 제공하는 java.util 패키지에 있는 컬렉션 프레임워크들은 대부분 스레드 안전(Thread Safe) 하지 않다.

 

우리가 일반적으로 사용하는 ArrayList , LinkedList , HashSet , HashMap 등 수많은 기본 자료 구조들은 내 부에서 수많은 연산들이 함께 사용된다. 배열에 데이터를 추가하고 사이즈를 변경하고, 배열을 새로 만들어서 배열의 크기도 늘리고, 노드를 만들어서 링크에 연결하는 등 수많은 복잡한 연산이 함께 사용된다.

 

그렇다면 처음부터 모든 자료 구조에 synchronized를 사용해서 동기화를 해두면 어떨까? synchronized , Lock , CAS 등 모든 방식은 정도의 차이는 있지만 성능과 트레이드오프가 있다. 결국 동기화를 사용하지 않는 것이 가장 빠르다.

그리고 컬렉션이 항상 멀티스레드에서 사용되는 것도 아니다. 미리 동기화를 해둔다면 단일 스레드에서 사용할 때 동기 화로 인해 성능이 저하된다. 따라서 동기화의 필요성을 정확히 판단하고 꼭 필요한 경우에만 동기화를 적용하는 것이 필 요하다.

 

참고: 과거에 자바는 이런 실수를 한번 했다. 그것이 바로 java.util.Vector 클래스이다. 클래스는 지금의 ArrayList와 같은 기능을 제공하는데, 메서드에 synchronized를 통한 동기화가 되어 있다. 쉽게 이야 기해서 동기화된 ArrayList이다. 그러나 이에 따라 단일 스레드 환경에서도 불필요한 동기화로 성능이 저하되었고, 결과적으로 Vector는 널리 사용되지 않게 되었다. 지금은 하위 호환을 위해서 남겨져 있고 다른 대안이 많기 때문에 사용을 권장하지 않는다.

 

좋은 대안으로는 우리가 앞서 배운 것처럼 synchronized를 대신 적용해 주는 프록시를 만드는 방법이 있다. List , Set , Map 등 주요 인터페이스를 구현해서 synchronized를 적용할 수 있는 프록시를 만들면 된다. 이 방법을 사용하면 기존 코드를 그대로 유지하면서 필요한 경우에만 동기화를 적용할 있다.

 

실행 코드 - Syncronized 컬렉션을 프록시로 만듬
실행 결과

  • 클라이언트 -> ArrayList
    • 클라이언트 -> SynchronizedRandomAccessList (프록시) -> ArrayList
  • 클래스의 add() 메서드를 보면 synchronized 코드 블럭을 적용하고, 그 다음에 원본 대상의 add()를 호출하는 것을 확인할 있다.
  • Collections는 다음과 같이 다양한 synchronized 동기화 메서드를 지원한다. 메서드를 사용하면 List , Collection , Map , Set 다양한 동기화 프록시를 만들어낼 있다.
    • synchronizedList()
    • synchronizedCollection()
    • synchronizedMap()
    • synchronizedSet()
    • synchronizedNavigableMap()
    • synchronizedNavigableSet()
    • synchronizedSortedMap()
    • synchronizedSortedSet()

synchronized 프록시 방식의 단점

하지만 synchronized 프록시를 사용하는 방식은 다음과 같은 단점이 있다.

  • 첫째, 동기화 오버헤드가 발생한다. 비록 synchronized 키워드가 멀티스레드 환경에서 안전한 접근을 보장하지만, 각 메서드 호출 시마다 동기화 비용이 추가된다. 이로 인해 성능 저하가 발생할 수 있다.
  • 둘째, 전체 컬렉션에 대해 동기화가 이루어지기 때문에, 잠금 범위가 넓어질 수 있다. 이는 잠금 경합(lock contention)을 증가시키고, 병렬 처리의 효율성을 저하시키는 요인이 된다. 모든 메서드에 대해 동기화를 적용하 다 보면, 특정 스레드가 컬렉션을 사용하고 있을 때 다른 스레드들이 대기해야 하는 상황이 빈번해질 수 있다.
  • 셋째, 정교한 동기화가 불가능하다. synchronized 프록시를 사용하면 컬렉션 전체에 대한 동기화가 이루어지지만, 특정 부분이나 메서드에 대해 선택적으로 동기화를 적용하는 것은 어렵다. 이는 과도한 동기화로 이어질 수 있다.

쉽게 이야기해서 방식은 단순 무식하게 모든 메서드에 synchronized를 걸어버리는 것이다. 따라서 동기화에 최적화가 이루어지지 않는다. 자바는 이런 단점을 보완하기 위해 java.util.concurrent 패키지에 동시성 렉션(concurrent collection) 제공한다.

728x90