⬛ 동시성 컬렉션이 필요한 이유
원자적 연산; 여러 스레드가 동시에 접근해도 안전하다.
그럼 아래에서 add(..) 는 원자적 연산일까?
public class SimpleListMainV0 {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
// 스레드1, 스레드2가 동시에 실행 가정
list.add("A"); //스레드1 실행 가정
list.add("B"); // 스레드2 실행 가정
System.out.println(list);
}
}
위의 add(...)를 자세히 들여다보고, 다시 질문에 대답해보자. 아래는 원자적 연산인가?
public void add(Object e) {
elementData[size] = e;
sleep(100); // 멀티스레드 문제를 쉽게 확인하는 코드
size++;
}
답은 '아니다'. 위 add(...)는 결국 아래의 연산들을 포함한다.
1. 배열에 데이터 추가
2. size = size + 1
⬛ 멀티스레드 환경에서의 add(...) 예제 코드
A,B를 각각 데이터 추가 스레드를 만들어 실행해보자.
public class SimpleListMainV2 {
public static void main(String[] args) throws InterruptedException{
test(new BasicList());
}
private static void test(SimpleList list) throws InterruptedException {
log(list.getClass().getSimpleName());
// A를 리스트에 저장하는 코드
Runnable addA = new Runnable() {
@Override
public void run() {
list.add("A");
log("Thread-1: list.add(A)");
}
};
// B를 리스트에 저장하는 코드
Runnable addB = new Runnable() {
@Override
public void run() {
list.add("B");
log("Thread-2: list.add(B)");
}
};
Thread thread1 = new Thread(addA, "Thread-1");
Thread thread2 = new Thread(addB, "Thread-2");
thread1.start();
thread2.start();
thread1.join(); // thread1종료까지 기다림
thread2.join();
log(list);
}
}
우리는 Array리스트에게 데이터를 차곡차곡 넣어 달라고 명령했다.
그런데 갑자기 두 명의 부하, 스레드 A와 스레드 B가 동시에 접근해서 각자 데이터를 추가하려고 한다.
"저 먼저!", "나도 해야 해!"라고 소리치며 엉망진창이 되어버린다!
결국 Array리스트는 데이터를 제대로 저장하지 못하고, 데이터가 사라져 버리는 사태가 발생했다.
위로서 알수 있는 사실! 컬렉션 프레임워크 대부분은 스레드 세이프 하지 않다.
따라서 멀티 스레드 상황에서는 java.util 패키지의 일반적 컬렉션들은 사용하면 안된다.
⬛ 동기화의 등장

이때, 동기화 요정이 등장한다.
동기화 요정은 "걱정하지 마, 이제 내가 하나씩 순서대로 처리할 수 있게 도와줄게!"라며 마법의 주문 synchronized를 걸어 스레드 A와 B가 차례대로 일을 처리하게 만든다. 덕분에 Array리스트는 다시 안정적으로 작동가능해진다!
하지만 동기화 요정이 말하길, "내 주문은 강력하지만 느려질 수 있어. 너무 자주 쓰면 힘들어!"
하지만 우리의 동기화 요정이 마법을 부리려면...결국 모든 컬렉션을 다 복사해 각 메소드에 synchronized를 붙여줘야한다.
너무 비효율적이고,번거롭다!
⬛ 프록시의 도움
🟢 프록시 (proxy)
- 대리자, 대신 처리해주는 자
-
어떤 객체에 대한 접근을 제어하기 위해 그 객체의 대리인 또는 인터페이스 역할을 하는 객체를 제공하는 패턴
- 프록시 패턴을 사용해 기존 컬렉션을 수정하지 않고 동기화 기능만 추가할 수 있다.
🔶 어레이리스트 프록시 코드 예제
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);
}
@Override
public synchronized String toString() {
return target.toString() + " by " + this.getClass().getSimpleName();
}
}

우리의 프록시 요정은 말한다.
"걱정 마. 나는 마법을 부리지 않고도 동기화처럼 안전하게 데이터를 처리하게 해주지!"라며 프록시 패턴을 선사한다...
위처럼 타겟의 기능을 호출할때 앞뒤로 synchronized기능을 걸어주어, 실제 타겟리스트에 손대지 않고도 안전하게 작업이 가능하다.
또한, 클라이언트는 SimpleList의 구현체가 뭐든간에 SimpleList라는 인터페이스에만 의존한다.
이것을 추상화에 의존한다고 표현하는데, 이로써 어떤 구현체든 받을 수 있어 매우 유연해진다.
⬛ 자바 동시성 컬렉션1 - synchronized
다행히 자바는 컬렉션을 위한 프록시 동기화 기능(synchronized)을 제공한다!
🟢 Collections synchronized 동기화 프록시 메서드
- synchronizedList()
- synchronizedCollection()
- synchronizedMap()
- synchronizedSet()
- synchronizedNavigableMap()
- synchronizedNavigableSet()
- synchronizedSortedMap()
- synchronizedSortedSet()
🟢 Collections synchronized 동기화 프록시 메서드의 단점
- 성능저하: synchronized는 한번에 하나의 작업만 처리해 동기화 비용이 추가된다.
- 과도한 동기화: 또한 synchronized는 컬렉션 전체에 동기화를 적용하기에, 특정부분이나 메서드에 선택적 동기화가 어렵다.
⬛ 자바 동시성 컬렉션2 - 동시성 컬렉션

이로써 자바 1.5에 강력한 전사가 등장한다.
더 정교한 잠금 메커니즘과 다양한 성능 최적화 기법,그리고 유연한 동기화 전략을 제공하는 동시성 컬렉션들이 등장한다!
"나에게 맡겨! 나는 성능도 높이고, 데이터도 안전하게 지킬 수 있어!"
⬛ 동시성 컬렉션 종류와 예시
다양한 동시성 컬렉션을 활용하면, 멀티스레드 환경에서 안전하게 컬렉션을 사용할 수 있다. 각 컬렉션은 상황에 맞게 선택하여 성능과 안정성을 모두 확보할 수 있다.
1. ConcurrentHashMap
멀티스레드 환경에서 HashMap을 대체하는 동시성 맵.
여러 스레드가 동시에 데이터를 읽고 쓰는 상황에서도 성능 저하 없이 작동하도록 설계됨.
Map<String, Integer> map = new ConcurrentHashMap<>();
map.put("apple", 1);
map.put("banana", 2);
System.out.println(map.get("apple"));
2. CopyOnWriteArrayList
쓰기 작업이 일어날 때 배열의 복사본을 만들어 안전하게 데이터를 추가하거나 수정할 수 있도록 하는 리스트.
주로 읽기 작업이 많고, 쓰기 작업이 적은 환경에서 유용.
List<String> list = new CopyOnWriteArrayList<>();
list.add("Hello");
list.add("World");
System.out.println(list);
3. CopyOnWriteArraySet
CopyOnWriteArrayList를 기반으로 만들어진 세트.
쓰기 작업이 적고 읽기 작업이 빈번한 환경에서 사용됨.
Set<String> set = new CopyOnWriteArraySet<>();
set.add("A");
set.add("B");
System.out.println(set);
4. ConcurrentLinkedQueue
설명: 비차단(non-blocking) 방식으로 동작하는 스레드 안전한 큐.
요소를 큐에 삽입하거나 제거하는 작업이 동시에 발생해도 안전하게 처리됨.
Queue<Integer> queue = new ConcurrentLinkedQueue<>();
queue.offer(1);
queue.offer(2);
System.out.println(queue.poll());
5. LinkedBlockingQueue
고정된 크기를 가진 블로킹 큐로, 생산자-소비자 패턴에서 많이 사용됨.
큐가 꽉 차면 생산자는 대기하고, 큐가 비면 소비자는 대기하는 방식.
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(10);
queue.put(1);
System.out.println(queue.take());
6. ConcurrentSkipListMap
멀티스레드 환경에서 TreeMap을 대체하는 동시성 맵.
정렬된 맵이 필요할 때 사용됨.
Map<Integer, String> map = new ConcurrentSkipListMap<>();
map.put(2, "B");
map.put(1, "A");
System.out.println(map);
7. ConcurrentSkipListSet
ConcurrentSkipListMap을 기반으로 한 동시성 세트.
정렬된 세트가 필요할 때 사용.
Set<Integer> set = new ConcurrentSkipListSet<>();
set.add(3);
set.add(1);
System.out.println(set);
8. DelayQueue
요소가 지정된 시간이 지나야 소비될 수 있는 큐
작업을 일정 시간이 지난 후에 처리해야 하는 스케줄링 작업에 유용
DelayQueue<Delayed> queue = new DelayQueue<>();
// 요소 추가 및 딜레이 설정
동시성 컬렉션은 Collections.synchronizedXXX보다 더 좋은 성능을 제공한다.
하지만 동시성은 성능과 트레이드 오프가 있음에 주의하자.
단일 스레드를 사용하는 경우에는 일반 컬렉션을 사용해야 한다.
'Java' 카테고리의 다른 글
[Java] 김영한의 실전 자바 - 고급 1편 섹션12 스레드 풀과 Executor 프레임워크 (0) | 2024.09.29 |
---|---|
[Java] 김영한의 실전 자바 - 고급 1편 섹션9 생산자 소비자 문제2 (1) | 2024.09.26 |
[Java] 김영한의 실전 자바 - 고급 1편 섹션8 생산자 소비자 문제1 (0) | 2024.09.20 |
[Java] 김영한의 실전 자바 - 고급 1편 섹션7 고급 동기화 - concurrent.Lock (4) | 2024.08.28 |
[Java] 김영한의 실전 자바 - 고급 1편 섹션6 동기화 - synchronized (0) | 2024.08.19 |