이번 포스트에서는 멀티 스레드 환경에서 스레드 안전한(thread safe) 컬렉션 사용에 대해 살펴보자.
스레드와 Collection
Thread Safe or Not!
구 버전(JDK 1.2 이전)의 collection들인 Vector, Hashtable들은 이미 모든 메서드에 synchronized가 선언되어 있다. 따라서 매서드 실행 시마다 lock을 확인하는 절차가 필요하고 한 번에 하나의 스레드만 동작할 것이고 덕분에 멀티 스레드 환경에 안전하다. 하지만 성능의 문제가 발생할 수 밖에 없다. 만약 싱글 스레드 환경에서 이들을 썼다면 장점은 없고 단점만 남을 것이다.
JDK 1.2 이후에는 이들의 개선 버전으로 ArrayList, HashMap을 사용한다. 이들은 synchronized가 없다. 따라서 lock을 확인하는 절차가 생략되어 속도가 매우 빠르다. 하지만 멀티 스레드 환경에서 이들을 썼다면 데이터는 엉망이 될 수 있다.
public class ThreadSafeTest {
static int THREAD_CNT=5;
public static void test(List<Integer> list) throws InterruptedException {
Runnable r = () -> {
try {
for (int i = 0; i < 10000; i++) {
list.add(i);
}
} catch (Exception e) {
String collection = list.getClass().getSimpleName();
String threadName = Thread.currentThread().getName();
String exceptionInfo = e.getClass().getName() + " : " + e.getMessage();
System.out.printf("컬렉션: %s, 스레드: %s, 예외: %s%n", collection, threadName, exceptionInfo);
}
};
List<Thread> threads = IntStream.rangeClosed(1, THREAD_CNT).mapToObj(i -> new Thread(r)).collect(Collectors.toList());
threads.forEach(Thread::start);
threads.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.printf("자료 개수: %10d, \t타입: %s\n", list.size(), list.getClass());
}
public static void main(String[] args) throws InterruptedException {
List<List<Integer>> lists = List.of( new Vector<>(), new ArrayList<>(), Collections.synchronizedList(new ArrayList<>()));
for (int i = 0; i < lists.size(); i++) {
test(lists.get(i));
}
}
}
// 출력 예시 1
자료 개수: 50000, 타입: class java.util.Vector
자료 개수: 15840, 타입: class java.util.ArrayList
// 출력 예시 2
자료 개수: 50000, 타입: class java.util.Vector
컬렉션: ArrayList, 스레드: Thread-5, 예외: java.lang.ArrayIndexOutOfBoundsException : Index 149 out of bounds for length 109
컬렉션: ArrayList, 스레드: Thread-8, 예외: java.lang.ArrayIndexOutOfBoundsException : Index 149 out of bounds for length 109
컬렉션: ArrayList, 스레드: Thread-9, 예외: java.lang.ArrayIndexOutOfBoundsException : Index 149 out of bounds for length 109
컬렉션: ArrayList, 스레드: Thread-7, 예외: java.lang.ArrayIndexOutOfBoundsException : Index 149 out of bounds for length 109
컬렉션: ArrayList, 스레드: Thread-6, 예외: java.lang.ArrayIndexOutOfBoundsException : Index 149 out of bounds for length 109
자료 개수: 149, 타입: class java.util.ArrayList
비동기 Colleection을 동기로 사용하기
그럼 어떻게 해야 멀티 스레드 환경에서 안전하게 Collection을 사용할 수 있을까?
멀티 스레드 환경에서 안전하게 Collection을 사용하기 위해서는 synchronizedXXX 형태의 메서드를 이용한다. 이 메서드들은 비동기화된 컬렉션을 동기화된 컬렉션으로 변경해준다. 따라서 평소에는 비동기로 사용하다가 동기화가 필요한 시점에 살짝 감싸서 사용하면 된다.
메서드 명 |
선언부와 설명 |
synchronizedCollection () |
public static <T> Collection<T> synchronizedCollection(Collection<T> c) |
c를 이용해 SynchronizedCollection를 생성하고 리턴한다. |
synchronizedList () |
public static <T> List<T> synchronizedList(List<T> list) |
list를 이용해 SynchronizedList 또는 SynchronizedRandomAccessList를 생성하고 리턴한다. |
synchronizedSet () |
public static <T> Set<T> synchronizedSet(Set<T> s) |
s를 이용해 SynchronizedSet를 생성하고 리턴한다. |
synchronizedMap () |
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) |
m을 이용해 SynchronizedMap를 생성하고 리턴한다. |
사용법은 단순히 메서드의 파라미터로 Collection을 넘겨주면 된다.
public static void main(String[] args) throws InterruptedException {
List<List<Integer>> lists = List.of( new Vector<>(),
Collections.synchronizedList(new ArrayList<>()));
for (int i = 0; i < lists.size(); i++) {
test(lists.get(i));
}
}
애초에 동기 Collection 사용하기
이쯤이면 애초에 동기 Collection이 필요한 경우라면 Vector를 쓰는것도 무방하지 않을까?라고 생각할 수 있다. 앞서 이야기 했듯이 Vector는 불필요한 곳에까지 synchronized가 되어있으며 동시성 처리 방식도 최근의 방식과 차이가 있다. 또한 개별 메서드는 thread-safe 하지만 복합 연산(검사 후 수정)에서는 여전히 동기화 문제가 발생할 수 있다.
자바에서는 java.util.concurrent 패키지에서 일부 Collection에 대해 동기 Collection을 제공한다.
이 Collection들이 강조하는 부분은 synchronized(동기)가 아닌 concurrent (동시)이다. concurrent collection들은 멀티 스레드에 대해 안전하면서도 단일 제외 잠금(single exclusion lock)의 적용을 받지 않고 부분 잠금을 이용한다. 예를 들어 collection에 여러 개의 자료가 있을 때 synchronized는 전체를 하나의 스레드가 전체를 독점적으로 사용하는 반면 concurrent는 특정 요소만 독점적으로 사용하게 한다.
새로운 동기화된 Collection |
|
ConcurrentHashMap |
멀티 스레드 환경에서 동기화된 HashMap 보다 유리 |
ConcurrentSkipListMap |
멀티 스레드 환경에서 동기화된 TreeMap 보다 유리 |
ConcurrentSkipListSet |
멀티 스레드 환경에서 동기화된 TreeSet 보다 유리 |
CopyOnWriteArrayList |
멀티 스레드 환경에서 읽기 및 탐색 회수가 업데이트 보다 훨씬 많을 때 동기화된 ArrayList 보다 유리 |
CopyOnWriteArraySet |
멀티 스레드 환경에서 읽기 및 탐색 회수가 업데이트 보다 훨씬 많을 때 동기화된 Set 보다 유리 |
ConcurrentLinkedQueue |
... |
ConcurrentLinkedDeque |
... |