백엔드 엔지니어 이재혁
[Java] 컬렉션 프레임워크와 동시성 본문
기본 컬렉션 프레임워크는 동시성 문제를 일으킬 수 있다.
예시) `ArrayList`의 `add()` 메서드는 데이터를 추가할 뿐 아니라, `size`에 새로운 값도 대입하는 등 원자적인 연산이 아니다.
// ArrayList 클래스의 add 메서드들 중 하나
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
해결 방법
1. 프록시 패턴
동시성 처리가 되지 않은 기본 객체는 그대로 두고, 아래와 같이 그 객체의 인터페이스 구현체를 만든다.
public class SyncProxyList implements SimpleList {
private SimpleList target;
public SyncProxyList(SimpleList target) {
this.target = target;
}
@Override
public synchronized int size() {
return this.target.size();
}
// ...
}
위 `size()` 메서드와 같이 메서드들에 `synchronized` 키워드를 붙이고,
`target`의 메서드를 `synchronized` 내부 메서드 안에서 호출하도록 한다.
사용 예시) 아래와 같이 기본 객체를 생성하고, 그 객체를 프록시 객체의 매개변수로 전달해준다.
SimpleList syncedList = new SyncProxyList(new BasicList());
Java 기본 패키지 사용
java.util 패키지의 `Collections` 클래스에서 제공하는 프록시 기능
List<String> list = Collections.synchronizedList(new ArrayList<>());
`List` 뿐 아니라 다른 컬렉션 객체들도 사용할 수 있다. `Collections.synchronizedxxx(new xxx());`
각 리스트/맵/해시맵 등의 기본 객체들의 메서드에 프록시 패턴으로 `synchronized`를 걸어준다.
한계
다만, 이전에 배운 동시성 제어에서도 느꼈지만, synchronized를 무식하게 거는 방법은 성능 저하가 크다.
특히 컬렉션 프레임워크 메서드 안에서도 임계 구역을 좁힐 수 있는 여지가 있는데도 작용하지 못하고, CAS 연산 등의 다양한 성능 개선 방법을 적용시킬 수 없다.
2. 동시성 컬렉션 프레임워크
동시성 제어 최적화를 하지 못하는 proxy 패턴의 synchronized 적용법 대신 동시성 제어를 하면서도 성능 최적화도 해낸 기본 패키지를 사용할 수 있다.
List, Set, Map
| 종류 | 기본 자료구조 | 기타 | |
| List | CopyOnWriteArrayList | ArrayList | |
| Set | CopyOnWriteArraySet | HashSet | |
| ConcurrentSkipListSet | TreeSet | 정렬 상태 유지 / Comparator 가능 | |
| Map | ConcurrentHashMap | HashMap | |
| ConcurrentSkipListMap | TreeMap | 정렬 상태 유지 / Comparator 가능 | |
Queue (Deque)
| 종류 | 특징 | 기타 | |
| Queue | ConcurrentLinkedQueue | 동시성 큐, 논블로킹 큐 | |
| Deque | ConcurrentLinkedDeque | 동시성 데크, 논블로킹 큐 | |
| BlockingQueue (스레드 차단) |
ArrayBlockingQueue | 크기가 고정된 블로킹 큐 | 공정(fair)모드 사용 가능 |
| LinkedBlockingQueue | 크기가 무한하거나 고정된 블로킹 큐 | ||
| PriorityBlockingQueue | 우선순위가 높은 요소를 먼저 처리하는 블로킹 큐 | ||
추가 BlockingQueue
`SynchronousQueue`
데이터를 저장하지 않는 블로킹 큐로, 생산자가 데이터를 추가하면 소비자가 그 데이터를 받을 때까지 대기한다. 생산자-소비자 간의 직접적인 핸드오프(hand-off) 메커니즘을 제공한다. 쉽게 이야기해서 중간에 큐 없이 생산자, 소비자가 직접 거래한다.
`DelayQueue`
지연된 요소를 처리하는 블로킹 큐로, 각 요소는 지정된 지연 시간이 지난 후에야 소비될 수 있다. 일정 시간이 지난 후 작업을 처리해야 하는 스케줄링 작업에 사용된다.
자료 구조 다시 정리
자료구조 내용을 다시 한 번 정리해야 할 것 같아서 Java의 자료구조 내용을 전체적으로 다시 정리해봤다.
'Java' 카테고리의 다른 글
| [JUnit] Controller 단위 테스트 작성하기, 그리고 회고 (1) | 2025.06.29 |
|---|---|
| [Java] Executor 프레임워크 기본 (0) | 2025.06.25 |
| [JAVA] CAS 락 구현 (0) | 2025.06.10 |
| [Java] CAS 연산 활용 (1) | 2025.06.09 |
| [Java] Atomic과 CAS 연산 (0) | 2025.06.09 |