728x90
반응형
동시성 컬렉션이 필요한 이유
java.util
패키지에 소속되어 있는 컬렉션 프레임워크들은 스레드 세이프(thread-safe)하지 않다!- ⇒ 원자적인 연산처럼 느껴지는 메서드들의 대부분은 내부를 확인해보면 모두 여러개의 연산들로 이루어져 있으며 이들은 동시성 문제를 유발할 수 있다..
ArrayList
에 값을 추가하는 경우만 해도, 내부에 있는 배열에 데이터를 추가하고,size
도 증가시키는 것과 같은 작업을 해야하며,size
를 증가시키기 위한size++
연산 자체도 원자적이지 않다.
⇒ 멀티스레드 상황에서 여러 스레드가 동시에 컬렉션에 접근하는 경우라면 절대 java.util 패키지가 제공하는 일반적인 컬렉션들은 사용하면 안된다! (물론 일부 예외도 있다)
- 이렇게 원자적이지 않은 연산을 멀티스레드 상황에 안전하게 사용하려면
synchronized
,Lock
등을 사용해서 동기화를 해야한다. - 하지만, 이미
java.util
에 구현된 모든 컬렉션 프레임워크를 대상으로synchronized
키워드를 붙인 새로운 버전을 만들기에는 중복되는 코드들이 너무나도 많이 발생할 것이다. 구현이 변경되면 코드를 2곳에서 변경해야 할 것이다.
⇒ 자바에서는 이를 해결하기 위해 프록시 패턴을 도입하여 이러한 문제를 해결했다.
자바 동시성 컬렉션 - synchronized
- 모든 자료 구조에 synchronized를 대신 적용해주는 프록시를 만들어서 제공한다면 기존 코드를 그대로 유지하면서 필요한 경우에만 동기화를 적용할 수 있다.
- ⇒ 자바는 컬렉션을 위한 프록시 기능을 제공
- Collections는 다음과 같이 다양한
synchronized
동기화 메서드를 지원한다. 이 메서드를 사용하면 List, Collection, Map, Set 등 다양한 동기화 프록시를 만들어낼 수 있다.synchronizedList()
List<String> list = Collections.synchronizedList(new ArrayList<>());
와 같이 사용할 수 있다.
synchronizedCollection()
synchronizedMap()
synchronizedSet()
synchronizedNavigableMap()
synchronizedNavigableSet()
synchronizedSortedMap()
synchronizedSortedSet()
synchronized 프록시 방식의 단점
하지만 synchronized
프록시를 사용하는 방식은 다음과 같은 단점이 있다.
- 첫째, 동기화 오버헤드가 발생한다. 비록
synchronized
키워드가 멀티스레드 환경에서 안전한 접근을 보장하지만, 각 메서드 호출 시마다 동기화 비용이 추가된다. 이로 인해 성능 저하가 발생할 수 있다. - 둘째, 전체 컬렉션에 대해 동기화가 이루어지기 때문에, 잠금 범위가 넓어질 수 있다. 이는 잠금 경합(lock contention)을 증가시키고, 병렬 처리의 효율성을 저하시키는 요인이 된다. 모든 메서드에 대해 동기화를 적용하다 보면, 특정 스레드가 컬렉션을 사용하고 있을 때 다른 스레드들이 대기해야 하는 상황이 빈번해질 수 있다.
- 셋째, 정교한 동기화가 불가능하다.
synchronized
프록시를 사용하면 컬렉션 전체에 대한 동기화가 이루어지지만, 특정 부분이나 메서드에 대해 선택적으로 동기화를 적용하는 것은 어렵다. 이는 과도한 동기화로 이어질 수 있다.
쉽게 이야기해서 이 방식은 단순 무식하게 모든 메서드에 synchronized
를 걸어버리는 것이다. 따라서 동기화에 대한 최적화가 이루어지지 않는다. 자바는 이런 단점을 보완하기 위해 java.util.concurrent
패키지에 동시성 컬렉션(concurrent collection
)을 제공한다.
자바 동시성 컬렉션 - 동시성 컬렉션
java.util.concurrent
패키지에는 고성능 멀티스레드 환경을 지원하는 다양한 동시성 컬렉션 클래스들을 제공한다.- ex)
ConcurrentHashMap
,CopyOnWriteArrayList
,BlockingQueue
등
- ex)
- 이 컬렉션들은 더 정교한 잠금 메커니즘을 사용하여 동시 접근을 효율적으로 처리하며, 필요한 경우 일부 메서드에 대해서만 동기화를 적용하는 등 유연한 동기화 전략을 제공한다.
- 여기에 다양한 성능 최적화 기법들이 적용되어 있는데,
synchronized
,Lock
(ReentrantLock
),CAS
, 분할 잠금 기술(segment lock)등 다양한 방법을 섞어서 매우 정교한 동기화를 구현하면서 동시에 성능도 최적화했다- 각각의 최적화는 매우 어렵게 구현되어 있기 때문에, 자세한 구현을 이해하는 것 보다는, 멀티스레드 환경에 필요한 동시성 컬렉션을 잘 선택해서 사용할 수 있으면 충분하다
동시성 컬렉션의 종류
List
CopyOnWriteArrayList
→ArrayList
대안
Set
CopyOnWriteArraySet
→HashSet
대안ConcurrentSkipListSet
→TreeSet
의 대안
Map
ConcurrentHashMap
→HashMap
대안ConcurrentSkipListMap
→TreeMap
의 대안
Queue
ConcurrentLinkedQueue
: 동시성 큐, 비차단(non-blocking) 큐이다.
Deque
ConcurrentLinkedDeque
: 동시성 덱, 비 차단(non-blocking) 덱이다.
BlockingQueue
스레드를 차단하는 블로킹 큐도 알아보자.
ArrayBlockingQueue
- 크기가 고정된 블로킹 큐
- 공정(fair) 모드를 사용할 수 있다. 공정(fair) 모드를 사용하면 성능이 저하될 수 있다.
LinkedBlockingQueue
- 크기가 무한하거나 고정된 블로킹 큐
PriorityBlockingQueue
- 우선순위가 높은 요소를 먼저 처리하는 블로킹 큐
SynchronousQueue
- 데이터를 저장하지 않는 블로킹 큐로, 생산자가 데이터를 추가하면 소비자가 그 데이터를 받을 때까지 대기한다. 생산자-소비자 간의 직접적인 핸드오프(hand-off) 메커니즘을 제공한다. 쉽게 이야기해서 중간에 큐 없이 생산자, 소비자가 직접 거래한다.
DelayQueue
- 지연된 요소를 처리하는 블로킹 큐로, 각 요소는 지정된 지연 시간이 지난 후에야 소비될 수 있다. 일정 시간이 지난 후 작업을 처리해야 하는 스케줄링 작업에 사용된다
728x90
반응형
'Java' 카테고리의 다른 글
[Java] 제네릭(Generics) (0) | 2025.01.07 |
---|---|
[Java] 스레드 풀 & ExecutorService (0) | 2025.01.07 |
[Java] CAS - 동기화와 원자적 연산 (1) | 2025.01.06 |
[Java] 스레드 - 생산자 소비자 문제 (0) | 2025.01.06 |
[Java] 스레드 동시성 문제 (0) | 2025.01.06 |