동시성 컬렉션
동시성 컬렉션
동시성 컬렉션이 필요한 이유
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개 늘림size값을 읽음size에 1을 더함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메서드를 사용해야 합니다.
BlockingQueueArrayBlockingQueue- 크기가 고정된 블로킹 큐
- 공정 모드를 사용하 수 있으나 성능이 저하될 수 있음
LinkedBlockingQueue- 크기가 무한하거나 고정된 블로킹 큐
PriorityBlockingQueue- 우선 순위가 높은 요소를 먼저 처리하는 블로킹 큐
SynchronousQueue- 데이터를 저장하지 않는 블로킹 큐
- 생상자와 소비자가 직접적인 핸드오프(hand-off) 메커니즘
DelayQueue- 지연된 요소를 처리하는 블로킹 큐
- 각 요소는 지정된 지연 시간이 지난 후에야 소비 될 수 있음
참고
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.