[Java] 동시성 컬렉션

MEUN·2024년 12월 1일
0

개요

java.util 패키지 내 컬렉션 프레임워크의 대부분은 thread-safe 하지 않다.
이러한 문제점을 자바에서는 어떻게 해결하였는지 살펴보자.



Vectorsynchronized

최초에 자바는 이러한 문제를 해결하기 위해 JDK 1.0에서 java.util 패키지에서 Vector 클래스를 지원하였다.
하지만 아래 코드와 같이 synchronized 를 통해 동기화를 구현하여 단일 스레드에서도 불필요한 성능 저하가 발생하게 되었고, 지금은 거의 사용되지 않게 되었다.
현재는 하위 호환을 위해 존재하나, 사용을 권장하지 않는다.

package java.util;

public class Vector<E>
    extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{

    @SuppressWarnings("serial") // Conditionally serializable
    protected Object[] elementData;
    
    protected int elementCount;
    protected int capacityIncrement;
    
    ... 중략 ...
    
    
    public synchronized void copyInto(Object[] anArray) {
        System.arraycopy(elementData, 0, anArray, 0, elementCount);
    }

    public synchronized int capacity() {
        return elementData.length;
    }
    
    public synchronized int size() {
        return elementCount;
    }
    
    public synchronized boolean isEmpty() {
        return elementCount == 0;
    }
    
    public synchronized int indexOf(Object o, int index) {
        if (o == null) {
            for (int i = index ; i < elementCount ; i++)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = index ; i < elementCount ; i++)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }
    
    ... 중략 ...
}


동시성 컬렉션

1) Collections.synchronizedXX()

프록시 패턴을 이용하여 Collection 타입 객체를 synchronized 블록 내에서 동작하도록 구현한 기능이다.

유관 메서드

synchronizedCollection()

public static <T> Collection<T> synchronizedCollection(Collection<T> c) {
	return new SynchronizedCollection<>(c);
}

synchronizedList()

public static <T> List<T> synchronizedList(List<T> list) {
    return (list instanceof RandomAccess ?
        new SynchronizedRandomAccessList<>(list) :
        new SynchronizedList<>(list));
}

synchronizedMap()

public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
	return new SynchronizedMap<>(m);
}

synchronizedSet()

public static <T> Set<T> synchronizedSet(Set<T> s) {
	return new SynchronizedSet<>(s);
}

synchronizedNavigableMap()

public static <K,V> NavigableMap<K,V> synchronizedNavigableMap(NavigableMap<K,V> m) {
	return new SynchronizedNavigableMap<>(m);
}

synchronizedNavigableSet()

public static <T> NavigableSet<T> synchronizedNavigableSet(NavigableSet<T> s) {
	return new SynchronizedNavigableSet<>(s);
}

synchronizedSortedMap()

public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m) {
	return new SynchronizedSortedMap<>(m);
}

synchronizedSortedSet()

public static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s) {
	return new SynchronizedSortedSet<>(s);	
}

NavigableMapNavigableSet이란?
주어진 검색 대상에 가장 가까운 일치 항목을 반환하는 탐색 메서드를 제공하기 위해 SortedMapSortedSet을 확장하였으며, 각각 정렬된 맵과 집합을 지원한다.
두 인터페이스의 대표적인 구현체로는 TreeMap, TreeSet이 있다.


프록시 패턴 이용 방식

SynchronizedList를 예시로 살펴보자.
구현체 내부에 list 멤버변수를 두어 생성자로 넘겨 받은 List 객체로 설정한다.

이후 get(), set(), add(), remove()와 같은 동작에서 synchronized 블록 내에서 기존 객체의 동작을 수행하도록 구현되어 있다.

편리하게 이용 가능하지만 동기화 오버헤드로 인한 성능 저하, 동기화 최적화가 불가하다는 한계가 존재한다.

static class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> {

	@SuppressWarnings("serial") // Conditionally serializable
	final List<E> list;

	SynchronizedList(List<E> list) {
		super(list);
		this.list = list;
	}
	SynchronizedList(List<E> list, Object mutex) {
		super(list, mutex);
		this.list = list;
	}

	public boolean equals(Object o) {
		if (this == o)
			return true;
		synchronized (mutex) {return list.equals(o);}
	}
	public int hashCode() {
		synchronized (mutex) {return list.hashCode();}
	}

	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);}
	}

	public boolean addAll(int index, Collection<? extends E> c) {
		synchronized (mutex) {return list.addAll(index, c);}
	}

	... 중략 ...
}


2) java.util.concurrent

LinkedHashSet, LinkedHashMap과 같이 입력 순서를 유지하는 동시에 thread-safeSet, Map 구현체는 제공하지 않는다.

List

CopyOnWriteArrayList

  • 모든 요소 변경과 관련된 연산(추가, 설정 등)이 기본 배열의 새 복사본을 만들어서 구현되는 ArrayListthread-safe한 버전
  • 일반적으로 비용이 많이 들지만 순회 연산이 변경과 관련된 연산보다 훨씬 많은 경우 다른 대안보다 효율적

Set

CopyOnWriteArraySet

  • 모든 연산에 내부 CopyOnWriteArrayList를 사용하는 Set
  • 일반적으로 집합 크기가 작고, 읽기 전용 연산이 쓰기 연산보다 훨씬 많으며, 순회하는 동안 스레드 간 간섭을 방지해야 하는 애플리케이션에 가장 적합
  • 요소 변경과 관련된 연산은 일반적으로 전체 기본 배열을 복사해야 하므로 비용이 많이 들어감

ConcurrentSkipListSet

  • ConcurrentSkipListMap에 기반한 확장 가능한 동시 탐색 가능한 구현체
  • 집합의 요소는 사용된 생성자에 따라 정렬된 상태로 유지 (Comparator 사용 가능)

Map

ConcurrentHashMap

  • 검색의 완전한 동시성과 업데이트에 대한 높은 기대 동시성을 지원하는 해시 테이블
  • 해시테이블과 동일한 기능을 제공하며, 해시테이블의 각 메서드에 대응하는 메서드 버전을 포함
  • 모든 연산이 thread-safe 하지만 검색 연산에는 락을 사용하지 않으며, 모든 액세스를 방지하는 방식으로 전체 테이블을 잠그는 기능은 지원되지 않음

ConcurrentSkipListMap

  • 확장 가능한 ConcurrentNavigableMap의 구현체
  • 사용된 생성자에 따라 정렬된 상태로 유지 (Comparator 사용 가능)

Queue

ConcurrentLinkedQueue

  • 연결된 노드를 기반으로 하는 thread-safe
  • 요소를 FIFO 방식으로 정렬 (큐의 헤드는 큐에 가장 오래 대기 중인 요소)
  • 효율적인 Non-Blocking 알고리즘을 사용

Deque

ConcurrentLinkedDeque

  • 연결된 노드를 기반으로 하는 thread-safe Deque
  • 컬렉션의 비동기적 특성으로 인해 현재 요소 수를 확인하려면 요소를 순회해야 하므로 순회 중에 컬렉션이 수정되면 부정확한 결과가 리턴될 수 있음
  • addAll(), removeIf() 또는 forEach()와 같이 여러 요소를 대량으로 추가, 제거 또는 조회하는 연산은 원자적으로 수행된다고 보장할 수 없음



참고 자료

0개의 댓글