동기화 리스트 사용하기 (synchronizedList vs copyOnWriteList)

list 동기화의 필요성

아마 이 글을 읽으시는 독자분들 께서는 멀티쓰레드 환경에서 list.add(...) 혹은 list.get(...) 을 수행할때 오류가 발생함을 경험해보셨을것이라 생각합니다.

일반적으로는 다른 쓰레드에서 list 내부 array의 size가 변경되면서 out-of-index로 인해 발생하는 exception 혹은, 경쟁이 일어나면서 기대하던 데이터를 가지고오지 못하는 현상일것입니다.

이는 쓰레드간에 경쟁을 하면서 list의 데이터가 변경되는 이유인데 해결하기 위해서는 경쟁 구간을 thread-safe하게 만들거나 가장 간단하게는, thread-safe 하게 구현된 list를 사용하는것이 해결방안 일 것입니다.

  • syncrhonizedList
  • copyOnWriteList

synchronizedList

먼저 synchronizedList을 살펴보겠습니다.

// java.util.Collections

...

static class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> {
    private static final long serialVersionUID = -7754090372962971524L;

    final List<E> list;

    ...

    public E get(int index) {
        synchronized (mutex) {return list.get(index);}
    }
    public E set(int index, E element) {
        synchronized (mutex) {return list.set(index, element);}
    }
    public void add(int index, E element) {
        synchronized (mutex) {list.add(index, element);}
    }
    public E remove(int index) {
        synchronized (mutex) {return list.remove(index);}
    }

    public int indexOf(Object o) {
        synchronized (mutex) {return list.indexOf(o);}
    }
    public int lastIndexOf(Object o) {
        synchronized (mutex) {return list.lastIndexOf(o);}
    }

    ...
}

java.util.CollectionsList를 상속받아 구현되어 있으며, 모든 함수에 Synchronized 키워드를 통해 쓰레드 세이프하게 사용 할 수 있도록 해줌을 보실 수 있습니다.


CopyOnWriteArrayList

다음으로 CopyOnWriteArrayList을 살펴보겠습니다.

// java.util.concurrent.CopyOnWriteArrayList

public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    private static final long serialVersionUID = 8673264195747942595L;

    final transient Object lock = new Object();

    private transient volatile Object[] array;

    ...

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

    public E set(int index, E element) {
        synchronized (lock) {
            Object[] es = getArray();
            E oldValue = elementAt(es, index);

            if (oldValue != element) {
                es = es.clone();
                es[index] = element;
            }
            setArray(es);
            return oldValue;
        }
    }

    public boolean add(E e) {
        synchronized (lock) {
            Object[] es = getArray();
            int len = es.length;
            es = Arrays.copyOf(es, len + 1);
            es[len] = e;
            setArray(es);
            return true;
        }
    }

    ...
}

위 코드에서 볼 수 있듯이 set, add 과정에서 lock과 함께 array를 copy 후 write하는 방식으로 처리를 하기때문에, get을 수행할때에는 thread-safe하게 기존의 데이터를 조회 할 수 있습니다.


synchronizedList VS CopyOnWriteArrayList

Silver bulllet은 없듯이, 모든곳에서 최적인 concurrent list는 없을것입니다. 그렇다면 어떤 list를 사용하는것이 이득일까요?

결론부터 정리해보자면 아래와 같습니다.

  • read 작업량 < write 작업량 : SynchronizedList
  • read 작업량 > write 작업량 : CopyOnWriteList

SynchronizedListget 작업에서도 락이 걸리기때문에 멀티 쓰레드 환경에서 조회가 많은 작업일경우에는 오버헤드가 더 클 것 입니다. CopyOnWriteList는 위에서도 살펴보았다시피 락과 함께 set, add 과정에서 데이터를 복제후 설정하는 방식으로 사용하기때문에 추가적인 오버헤드가 발생합니다. 따라서 멀티쓰레드 환경에서 어떤 작업이 주요로 수행되는가에 따라 여러분의 코드에 사용될 리스트를 선택하셔야 할 것 입니다.