본문 바로가기

개발/자바&스프링

동기화된 리스트를 빠르게 조회하기 - synchronizedList와 CopyOnWriteArrayList 성능 비교해봅시다

1. Intro
2. create
3. add
4. get
5. 결론

📌  INTRO

최근 멀티스레딩 프로그래밍을 하며 동시성 제어 기능이 구현된 클래스는

단지 동시성 제어 목적으로 아무거나 가져다 쓰면 안 된다는 것을 배웠습니다. 

 

synchronizedList와 CopyOnWriteArrayList를 비교해 보고 간단한 테스트를 해보겠습니다. 

 

📌  create

synchronizedList

 

인자로 넘겨받은 동기화되지 않은 리스트를 암묵적 락이 적용된 동기화된 클래스로 wrapping 해서 리턴해줍니다.

리스트를 새로 생성하지 않기 때문에 생성 비용이 크지 않습니다. 

CopyOnWriteArrayList

 

반면 CopyOnWriteArrayList는 리스트의 원소를 새로운 배열에 복사합니다. 

리스트의 크기가 많다면 복사 비용의 오버헤드가 발생합니다. 

 

500만 사이즈의 리스트로 생성에 소요된 시간을 비교해 보니 실제로 차이가 보입니다.

6ms로 아주 크진 않긴 합니다.

 

  synchronizedList CopyOnWriteArrayList
생성 시간 비교 9 ms 16 ms

📌  add

synchronizedList

public boolean add(E e) {
   synchronized (mutex) {return c.add(e);}
}

 

synchronized로 인해 락을 획득한 순서대로 순차적으로 add 작업을 수행합니다.

멀티스레드 환경이 아니라면 동기화된 리스트를 사용했을 때 성능 저하 지점이 됩니다!

 

 

CopyOnWriteArrayList

 

 

요소의 추가에도 배열의 복사 오버헤드가 발생합니다. 

변경 작업이 많다면 횟수만큼 배열 복사 작업이 발생되어 심각한 성능 저하가 발생하게 됩니다. 

 

50만 번 수행 synchronizedList CopyOnWriteArrayList
add 비교 114 ms 22 s 15 ms

 

50만 번의 배열 복사 작업이 발생한 CopyOnWriteArrayList 이 훨씬 느리네요.

CopyOnWriteArrayList의 add는 synchronized로 인한 순차 처리 + 배열 복사 비용까지 발생하니 오래 걸립니다.

📌  get

synchronizedList

public E get(int index) {
    synchronized (mutex) {return list.get(index);}
}

 

get() 메서드 역시 synchronized가 선언된 메서드를 호출하게 됩니다.

이번에도 락을 획득한 스레드가 순서대로 get 메서드를 호출하게 됩니다.

CopyOnWriteArrayList

public E get(int index) {
    return elementAt(getArray(), index);
}

 

COWAL의 get 메서드에는 동기화 장치가 없습니다.

병렬성을 높이기 위해 리스트의 불변객체를 외부에 제공하기 때문에 잠금 없이 빠르게 리스트를 순회할 수 있습니다.

 

 

조회 비교 결과는 CopyOnWriteArrayList 가 2배 이상 빠릅니다. 

 

1000만 번 조회 synchronizedList CopyOnWriteArrayList
get 비교 157 ms 64 ms

 

주의할 것은 iterator()가 아닌 반복문으로 원본 리스트를 순회한다면 로직에 따라 동기화 작업이

추가로 필요할 수 있습니다.

 

 

iterator 객체가 생성될 시점의 리스트 상태를 스냅숏으로 반환하기 때문에 잠금 없는 순회를 제공하는 것입니다.

원본 리스트를 반복문으로 순회한다면 로직에 따라서 동기화를 주의해야 하는 이유입니다.

 

📌  결론

1. 

CopyOnWriteArrayList은 동기화된 리스트에서 읽기 성능을 향상한 클래스입니다. 

다만 create과 add 부분에서 배열의 복사 오버헤드가 발생하기 때문에 

읽기 작업보다 리스트 요소 변경이 많거나 사이즈가 큰 리스트를 동기화가 필요하다면 배열 복사 오버헤드를 잘 살펴봐야 하고

synchronizedList이 더 적합할 것입니다. 

 

2.

추가로 CopyOnWriteArrayList의 Iterator() 역시 복사된 객체를 얻기 때문에

Iterator() 호출 이후에 원본 리스트에 변경된 내용은 조회할 수 없습니다. 

 


참고

[1]

도서 [자바 병렬 프로그래밍]