Java ‐ 동시성 컬렉션 - dnwls16071/Backend_Study_TIL GitHub Wiki
📚 동시성 컬렉션이 필요한 이유
원자적 연산(Atomic Operation) : 원자적 연산이란, 더 이상 쪼갤 수 없고, 중간에 다른 스레드가 개입할 수 없는 연산
1. 동시성 이슈
- 과정1
public void add(Object e) {
elementData[size] = e; // (1). 쓰레드1, 쓰레드2가 동시에 실행된다.
sleep(100);
size++;
}
-
쓰레드1, 쓰레드2가 동시에 실행이 된다.
- 쓰레드1이 수행되면서 elementData[0] = A가 된다.
- 동시에 쓰레드2가 수행되면서 elementData[0] = A -> B가 된다.
-
과정2
public void add(Object e) {
elementData[size] = e;
sleep(100); // (2). 쓰레드1, 쓰레드2가 동시에 실행된다.
size++;
}
- 과정3
public void add(Object e) {
elementData[size] = e;
sleep(100);
size++; // (3). 쓰레드1, 쓰레드2가 동시에 실행된다.
}
- (3)에서 2가지 상황이 발생할 가능성이 있다.
- 둘 중 쓰레드1이 먼저 실행되면 그 다음 쓰레드2가 실행되므로 사이즈의 값은 2가 된다.
- 두 쓰레드가 우연의 일치로 동시에 실행되면 사이즈의 값이 1이 나올 수 있다.
❗컬렉션 프레임워크 대부분은 쓰레드 세이프(Thread Safe)하지 않다.
- 일반적인 컬렉션들을 절대로 쓰레드 세이프하지 않다.
- 단일 쓰레드가 컬렉션에 접근하는 경우라면 아무런 문제가 되지 않지만 멀티쓰레드 상황에서 여러 쓰레드가 동시에 컬렉션에 접근하는 경우라면 일반적인 컬렉션들을 사용하면 안 된다.
2. 동기화
- 여러 쓰레드가 접근해야 한다면
synchronized
,Lock
등으로 안전한 임계 영역을 만들면 해결할 수 있다.
3. 프록시 도입
- 프록시(Proxy)란, 대리자를 말한다.
- 컬렉션에 대한 접근을 중간에서 가로채고 제어하는 패턴으로 원본 컬렉션을 직접 노출시키지 않고 프록시 객체를 통해 간접적으로 접근하게 한다.
- 동기화 프록시는 모든 메서드 호출에 동기화 오버헤드가 발생한다. 특히 읽기 작업이 많은 상황에서 성능 병목이 될 수 있다. 또한 컬렉션 전체에 대해 하나의 락을 사용하기 때문에 동시성 수준이 다소 제한된다.
- 최신 자바에서는
concurrent
패키지의 전용 동시성 컬렉션 사용을 권장하는데 이들은 프록시 패턴보다 더 정교한 락-프리 알고리즘을 사용하여 높은 성능과 동시성을 제공한다. - 동기화 프록시는 기존 코드 호환성을 유지하면서도 쓰레드 안전성을 추가하는 간단한 방법이지만 성능이 중요한 애플리케이션에서는 전용 동시성 컬렉션을 사용하는 것이 더 효과적이다.
- 프록시 패턴은 객체지향 디자인 패턴 중 하나로 어떤 객체에 대한 접근을 제어하기 위해 그 객체의 대리인 또는 인터페이스 역할을 하는 객체를 제공하는 패턴이다. 프록시 객체는 실제 객체에 대한 참조를 유지하면서 그 객체에 접근하거나 행동을 수행하기 전에 추가적인 처리를 할 수 있도록 한다.
- 스프링 생태계에서는 프록시 패턴이 적용되는 대표적인 사례가 바로 AOP이다.
📚 자바 동시성 컬렉션 해결1 - synchronized
synchronized
,Lock
,CAS
모든 방식은 정도의 차이가 있지만 성능과 트레이드 오프가 있다.- 결국 동기화를 사용하지 않는 것이 가장 빠르다.
❗synchronized 프록시 방식의 단점
- 첫째, 동기화 오버헤드가 발생한다. 비록
synchronized
키워드가 멀티쓰레드 환경에서 안전성을 보장하지만, 각 메서드 호출 시마다 동기화 비용이 수반된다. 이로 인해 성능 저하가 발생할 수 있다.- 둘째, 전체 컬렉션에 대해 동기화가 이루어지기 때문에, 잠금 범위가 넓어질 수 있다. 이는 잠금 경합을 증가시키고 병렬 처리 효율성을 저하시키는 요인이 된다. 모든 메서드에 대해 동기화를 적용하다 보면 특정 쓰레드가 컬렉션을 사용하고 있을 때 다른 쓰레드들이 대기해야 하는 상황이 빈번해질 우려가 있다.
- 셋째, 정교한 동기화가 불가능하다.
synchronized
프록시를 사용하면 컬렉션 전체에 대한 동기화가 이루어지지만, 특정 부분이나 메서드에 대해 선택적으로 동기화를 적용하는 것은 어렵다. 이는 과도한 동기화로 이어질 수 있다.
📚 자바 동시성 컬렉션 해결2 - 동시성 컬렉션
concurrent
패키지에는 고성능 멀티쓰레드 환경을 지원하는 다양한 동시성 컬렉션 클래스들을 제공한다.- 해당 컬렉션들은 더 정교한 잠금 메커니즘을 사용하여 동시 접근을 효율적으로 처리하며, 필요한 경우 일부 메서드에 대해서만 동기화를 적용하는 등 유연한 동기화 전략을 제공한다.
synchronized
,Lock
,CAS
, 분할 잠금 기술 등 다양한 방법을 섞어 매우 정교한 동기화를 구현하면서 동시에 성능도 최적화했다.
❗동시성 컬렉션의 종류
- List : CopyOnWriteArrayList
- Set : CopyOnWriteArraySet, ConcurrentSkipListSet
- Map : ConcurrentHashMap, ConcurrentSkipListMap
- Queue : ConcurrentLinkedQueue
- Deque : ConcurrentLinkedDeque
❗쓰레드를 차단하는 블로킹 큐의 종류
- ArrayBlockingQueue
- LinkedBlockingQueue
- PriorityBlockingQueue
- SynchronousQueue
- DelayQueue