본문으로 건너뛰기
김신건의 로그

[Java] CopyOnWriteArrayList

· 수정 · 📖 약 2분 · 959자/단어 #java #collection #data-structure #list #concurrent #thread-safe #copy-on-write
CopyOnWriteArrayList, CopyOnWriteList, CoW ArrayList, copy-on-write list, java.util.concurrent.CopyOnWriteArrayList

정의

java.util.concurrent.CopyOnWriteArrayList쓰기 시 배열 전체를 복사 하는 thread-safe List 구현. 읽기에는 lock 이 전혀 없고, 쓰기에는 ReentrantLock 으로 직렬화한다.

java.util.concurrent (JSR-166) 의 컬렉션. 읽기 압도적 다, 쓰기 거의 없음 인 시나리오 (예: 이벤트 리스너 목록, 옵저버 패턴, 설정 캐시) 에 가장 적합.

시각화

내부 구조

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

    final transient ReentrantLock lock = new ReentrantLock();
    private transient volatile Object[] array;

    final Object[] getArray() { return array; }
    final void setArray(Object[] a) { array = a; }
}

핵심 두 가지.

  • arrayvolatile: 한 스레드의 setArray() 가 다른 스레드의 getArray() 에 즉시 보인다. 별도 락 없이 새 배열 참조가 publish 된다.
  • lock 은 쓰기 직렬화용: 동시에 두 쓰기가 진행되지 않도록만 보장. 읽기에는 관여하지 않는다.

읽기, lock 없이

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

getArray() 로 현재 배열 참조를 한 번 읽고, 그 배열의 인덱스 자리를 반환한다. lock 도, 동기화 블록도 없다.

스레드가 100 개라도 모두 자기 CPU 의 캐시로 volatile 읽기 1 번을 수행할 뿐. 사실상 ArrayList 만큼 빠르다.

쓰기, 전체 복사

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);  // O(n)
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}
  • 락 획득: 다른 쓰기를 막는다 (읽기는 그대로 진행 가능)
  • Arrays.copyOf: 전체 배열을 통째로 복사. O(n) 비용.
  • 수정: 새 배열에 원소 추가
  • setArray: 새 배열 참조로 교체. 이 한 순간에 모든 후속 읽기가 새 배열을 보게 된다.

remove, set 도 같은 패턴. 모든 쓰기가 O(n) 복사 를 동반한다.

주요 연산 비용

메서드시간비고
get(int i)O(1)없음volatile read 1 번
size()O(1)없음
iterator()O(1)없음snapshot 반환
add(E)O(n)쓰기 락배열 전체 복사
add(int i, E)O(n)쓰기 락복사 + shift
remove(int i)O(n)쓰기 락복사 + shift
set(int i, E)O(n)쓰기 락한 자리만 바뀌어도 복사
addAll(Collection)O(n + m)쓰기 락 한 번묶어서 처리

TIP

여러 원소를 한 번에 추가할 때는 addAll(...) 을 쓰자. 각각 add 하면 n 회의 전체 복사가 발생하지만, addAll 은 한 번의 복사로 끝난다.

Snapshot Iterator

CopyOnWriteArrayList 의 iterator 는 생성 시점의 배열 참조를 그대로 들고 다닌다.

List<String> list = new CopyOnWriteArrayList<>(List.of("A", "B", "C"));

Iterator<String> it = list.iterator();
list.add("D");                   // 새 배열로 교체됨
while (it.hasNext()) {
    System.out.println(it.next());  // A, B, C 만 출력 (D 는 보이지 않음)
}

장점:

  • ConcurrentModificationException 없음, 순회 중 다른 스레드가 수정해도 안전
  • 순회 중 수정 자유, 단 그 수정은 다음 순회부터 보임

단점:

  • it.remove(), it.set(E), it.add(E) 모두 UnsupportedOperationException, iterator 는 read-only

메모리 관점

쓰기가 일어날 때마다 두 배열이 동시에 존재 한다 (기존 + 새것). GC 가 기존 배열을 회수할 때까지는 메모리 2 배 사용.

또한 진행 중인 iterator 가 기존 배열을 참조하고 있다면 그것도 살아 있어야 한다. 긴 iterator 가 많은 환경에서는 메모리 사용량이 더 늘어날 수 있다.

언제 CopyOnWriteArrayList 가 답인가

읽기 >> 쓰기 시나리오에서 최적이다.

패턴적합도
이벤트 리스너 목록 (등록 드물고 발화 잦음)⭐⭐⭐
옵저버 패턴 구독자 목록⭐⭐⭐
설정 / 메타데이터 캐시 (변경 드묾)⭐⭐⭐
읽기 위주의 작은 리스트⭐⭐
쓰기 빈도 적당, 컬렉션이 작음
쓰기 빈도 높음 / 컬렉션 큼✗ (ConcurrentLinkedQueue, Collections.synchronizedList 등 고려)

IMPORTANT

1000 개 원소 리스트에 매초 100 번 add 한다면 CoW 는 매초 100,000 회 배열 복사 = 10MB+ 메모리 트래픽. 이런 워크로드에서는 사용 금지.

다른 동시성 List 와의 비교

옵션읽기쓰기메모리iterator
CopyOnWriteArrayListlock-freeO(n) 복사쓰기 시 2xsnapshot, CME 없음
Collections.synchronizedList(arraylist)동일외부 동기화 필요, fail-fast
Vector동일외부 동기화 필요, fail-fast
ConcurrentLinkedDequelock-freelock-free노드 오버헤드weakly consistent

참고

이 글의 용어 (11개)
[Java] ArrayListjava
정의 는 동적 배열 (dynamic array) 로 구현된 . 내부적으로 를 들고 다니며, 용량이 부족해지면 새 배열로 옮긴다. Java 의 가장 흔한 컬렉션 구현체. 인덱스 기…
[Java] Collectionjava
정의 는 그룹으로 묶인 객체들을 표현하는 최상위 인터페이스. JCF (Java Collections Framework) 의 입구이자, / / / 모두 이를 확장한다. 자체는 직접…
[Java] ConcurrentHashMapjava
정의 는 고동시성 환경에서 사용 가능한 구현. JSR-166 (Java 5) 도입, Java 8 에서 내부 구조가 크게 재작성됐다. 과 같은 인터페이스를 제공하면서 thread-…
[Java] Fail-Fast Iteratorjava
정의 Fail-Fast Iterator 는 순회 도중 컬렉션의 구조가 변경되면 즉시 을 던지는 iterator. , , 등 대부분의 컬렉션의 iterator 가 이 방식. 목적은…
[Java] Iterablejava
정의 는 루프로 순회 가능한 모든 타입의 최상위 인터페이스. 단 하나의 추상 메서드, 를 정의한다. 인터페이스가 을 extends 하므로 , , , 등 모든 컬렉션이 자동으로 대…
[Java] Listjava
정의 는 순서가 있고 중복이 허용 되는 컬렉션을 표현하는 인터페이스. 각 원소는 0 부터 시작하는 정수 인덱스로 접근 가능. 인터페이스를 확장하며, 의 모든 성질을 물려받는다. …
[Java] Objectjava
정의 는 Java 의 모든 클래스의 최상위 부모 (root) 클래스. 가 명시되지 않은 클래스는 컴파일러가 자동으로 를 붙인다. 인터페이스는 클래스가 아니라 를 직접 상속하지는 …
[Java] ReentrantLockjava
정의 는 키워드와 같은 상호 배제 (mutual exclusion) 를 제공하는 클래스 기반 락. JSR-166 (Java 5) 에서 추가됐다. "재진입 (reentrant)" …
[Java] volatilejava
정의 은 Java 의 키워드. 필드에 붙이면 두 가지를 보장한다. 1. 가시성 (visibility): 한 스레드의 쓰기가 다른 모든 스레드에 즉시 보인다. CPU 캐시에 머무르…
Geometry Basic: 벡터, CCW, 외적, 내적algorithm
정의 기하 알고리즘의 밑바탕. 대부분의 2D/3D 기하 문제는 벡터 연산, 내적/외적, CCW 판정 세 가지의 조합으로 풀립니다. 벡터 2D 벡터: 두 점 $A = (ax, ay…
Non-Blocking (논블로킹)concurrency
정의 Non-Blocking 은 호출이 결과를 기다리지 않고 즉시 반환 하는 실행 방식. 결과가 준비되지 않았다면 "아직 안 됐다" 라는 신호 ( , , , 등) 를 반환하고, …

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기