포스트

동시성 컬렉션

동시성 컬렉션

동시성 컬렉션이 필요한 이유

1
2
3
4
5
6
7
8
9
10
11
public class SimpleListMainV0 {
  public static void main(String[] args) {
    List<String> list = new ArrayList<>();

    // 스레드1, 스레드2가 동시에 실행 가정
    list.add("A");
    list.add("B");
    System.out.println(list);

  }
}

위 코드는 멀티 스레드 환경에서 안전하지 않습니다. 그 이유는 add() 메서드가 원자적이지 않기 때문입니다.

1
2
3
4
public void add(Object e) {
  elementData[size] = e;
  size++;
}
  • 배열에 값을 추가
  • size를 1개 늘림
    1. size 값을 읽음
    2. size에 1을 더함
    3. size에 대입

synchronized를 이용하기

간단하게 해결하는 방법은 컬렉션 내의 메서드에 synchronized를 추가하는 것입니다.

그럼 멀티 스레드 환경에서 안전하게 사용할 수 있습니다.

그런데 ArrayList, LinkedList 등등 수 많은 컬렉션 프레임워크를 다 복사하여 만들어야 한다면 너무 중복 코드도 많고 힘들 것 같습니다.

프록시를 이용하여 synchronized 처리 쉽게 하기

프록시 란?

  • 우리말로 대리자, 대신 처리해주는 자라는 뜻
  • 어떤 객체에 대한 접근을 제어하기 위해 그 객체의 대리인 또는 인터페이스 역할을 하는 객체를 제공하는 패턴

주요 목적

  • 접근 제어: 실제 객체에 대한 접근을 제한하거나 통제
  • 성능 향상: 실제 객체의 생성을 지연시키거나 캐싱하여 성능을 최적화
  • 부가 기능 제공: 실제 객체에 추가적인 기능(로깅, 인증, 동기화 등)을 투명하게 제공

예제

1
2
3
4
5
6
7
public interface SimpleList {
  int size();

  void add(Object e);

  Object get(int index);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class BasicList implements SimpleList {

  private static final int DEFAULT_CAPATICY = 5;
  private Object[] elementData;
  private int size = 0;

  public BasicList() {
    this.elementData = new Object[DEFAULT_CAPATICY];
  }

  @Override
  public int size() {
    return size;
  }

  @Override
  public void add(Object e) {
    elementData[size] = e;
    sleep(100); // 멀티 스레드 문제를 쉽게 확인하는 코드
    size++;
  }

  @Override
  public Object get(int index) {
    return elementData[index];
  }

  @Override
  public String toString() {
    return Arrays.toString(Arrays.copyOf(elementData, size)) + " size: " + size + " capacity: " + elementData.length;
  }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class SyncProxyList implements SimpleList {

  private SimpleList target;

  public SyncProxyList(SimpleList target) {
    this.target = target;
  }

  @Override
  public synchronized int size() {
    return target.size();
  }

  @Override
  public synchronized void add(Object e) {
    target.add(e);
  }

  @Override
  public synchronized Object get(int index) {
    return target.get(index);
  }
}
1
2
3
4
5
public class SimpleListMainV2 {
  public static void main(String[] args) throws InterruptedException {
    test(new SyncProxyList(new BasicList()));
  }
}

BasicList의 기능을 똑같이 수행하면서 synchronized 기능을 추가하였습니다.

자바 동시성 컬렉션

synchronized

자바에서는 Collections.synchronizedXxx 을 통해 쉽게 synchronized 기능을 하는 컬렉션으로 변경할 수 있습니다.

1
List<String> list = Collections.synchronizedList(new ArrayList<>());

synchronized 프록시의 단점

  • 동기화 오버헤드 발생
  • 전체 컬렉션에 대해 동기화가 이루어지기 때문에, 잠금 범위가 넓어질 수 있음
  • 정교한 동기화가 불가능

동시성 컬렉션

자바의 동시성 컬렉션은 더 정교한 잠금 메커니즘을 사용하여 동시 접근을 효율적으로 처리하며, 필요한 경우 일부 메서드에 대해서만 동기화를 적용하는 등 유연한 동기화 전략을 제공합니다.

synchronized, Lock, CAS, 분할 잠금 기술 등 다양한 방법을 섞어서 매우 정교한 동기화를 구현하면서 동시에 성능도 최적화 했습니다.

동시성 컬렉션 종류

  • List
    • CopyOnWriteArrayList → ArrayList 대안
  • Set
    • CopyOnWriteArraySet → HashSet 대안
    • ConcurrentSkipListSet → TreeSet 대안
  • Map
    • ConcurrentHashMap → HashMap 대안
    • ConcurrentSkipListMap → TreeMap 대안
  • Queue
    • ConcurrentLinkedQueue → 동시성 큐, 비 차단(non-blocking) 큐
  • Deque
    • ConcurrentLinkedDeque → 동시성 데크, 비 차단(non-blocking) 큐

LinkedHashSet, LinkedHashMap 처럼 순서를 유지하는 동시에 멀티스레드 환경에서 사용할 수 있는 Set, Map 구현체는 제공하지 않습니다.

필요하다면 Collections.synchronizedXxx 메서드를 사용해야 합니다.

  • BlockingQueue
    • ArrayBlockingQueue
      • 크기가 고정된 블로킹 큐
      • 공정 모드를 사용하 수 있으나 성능이 저하될 수 있음
    • LinkedBlockingQueue
      • 크기가 무한하거나 고정된 블로킹 큐
    • PriorityBlockingQueue
      • 우선 순위가 높은 요소를 먼저 처리하는 블로킹 큐
    • SynchronousQueue
      • 데이터를 저장하지 않는 블로킹 큐
      • 생상자와 소비자가 직접적인 핸드오프(hand-off) 메커니즘
    • DelayQueue
      • 지연된 요소를 처리하는 블로킹 큐
      • 각 요소는 지정된 지연 시간이 지난 후에야 소비 될 수 있음

참고

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.