Java

자바의 Collection Framework - 동기화, 병렬 처리

마닐라 2022. 8. 9. 16:48

자바의 컬렉션 프레임워크의 대부분의 클래스들은 싱글 스레드 환경에서 사용될 수 있도록 설계 되었습니다.

그렇기 때문에 여러 스레드가 동시에 컬렉션에 접근한다면 의도하지 않게 요소가 변경될 수 있습니다.

 

Synchronized Collection

Vector와 Hashtable은 동기화된(synchronized) 메서드로 구성되어 있기 때문에 멀티 스레드 환경에서 안전하게 요소를 처리할 수 있지만 ArrayList, HashSet, HashMap은 동기화된 메서드로 구성되어 있지 않아 멀티 스레드 환경에서 안전하지 않습니다.

 

경우에 따라서는 ArrayList, HashSet, HashMap을 싱글 스레드 환경에서 사용하다가 멀티 스레드 환경으로 전달할 필요도 있을 것입니다.

이런 경우를 대비해서 컬렉션 프레임워크는 비동기화된 메서드를 동기화된 메서드로 래핑하는 Collections의 synchronizedXXX() 메서드를 제공하고 있습니다.

파라미터로 비동기화된 컬렉션을 대입하면 동기화된 컬렉션을 리턴합니다.

 

 

Concurrent Collection

동기화된 컬렉션은 멀티 스레드 환경에서 하나의 스레드가 요소를 안전하게 처리하도록 도와주지만, 전체 요소를 빠르게 처리하지는 못합니다.

하나의 스레드가 요소를 처리할 때 전체 잠금이 발생하여 다른 스레드는 대기 상태(blocked)가 되기 때문입니다.

그렇기 때문에 멀티 스레드가 병렬적으로 컬렉션의 요소들을 처리할 수 없습니다.

자바는 멀티 스레드가 컬렉션의 요소를 병렬적으로 처리할 수 있도록 특별한 컬렉션을 제공합니다.

java.util.concurrent 패키지에서 제공하고 있으며 제공하는 클래스의 일부를 어떤 방식으로 처리를 하는지에 대해서 알아보겠습니다.

 

CopyOnWriteArrayList

동기화된 ArrayList를 생성하는 방법은 두 가지가 있습니다.

1. Collections.synchronizedList()

2. CopyOnWriteArrayList

 

1번의 경우에는 JDK 1.2 버전에 추가 되었는데, 반환되는 컬렉션은 모든 읽기와 쓰기 동작에 대해 동기화가 되어 있습니다.

따라서 상당히 비효율적이고 이러한 점을 보완한 CopyOnWriteArrayList가 등장하게 됩니다.

읽기 동작

CopyOnWriteArrayList는 모든 쓰기 동작에 원본 배열에 있는 요소를 복사하여 임시 배열을 만들고, 이 임시 배열에 쓰기 동작을 수행한 후 원본 배열을 갱신합니다.

이 덕분에 읽기 동작은 잠금이 걸리지 않아 1번의 방법보다 성능이 더 좋습니다.

쓰기 동작

CopyOnWriteArrayList는 쓰기 동작의 경우에는 1번의 방법과 같이 잠금을 수행합니다.

이 때 CopyOnWriteArrayList는 임시 배열을 만드는 과정이 있으므로 1번의 방법보다 성능이 떨어집니다.

 

따라서 해당 컬렉션은 읽기 동작이 빈번한 경우에 사용하는 것이 효율적으로 볼 수 있습니다.

CopyOnWriteArraySet의 경우에도 자료구조의 특징만 제외하면 동일합니다.

 

ConcurrentHashMap

동기화된 HashMap을 생성하는 방법은 두 가지가 있습니다.

1. Collections.synchronizedMap()

2. ConcurrentHashMap

 

1번의 경우에도 synchronizedList()와 같이 모든 읽기와 쓰기 동작에 대해 동기화가 되어 있습니다.

ConcurrentHashMap은 각 테이블을 버킷을 독립적으로 잠그는 방식을 사용합니다.

빈 버킷에 요소를 삽입할 경우에는 잠금 대신 CAS 알고리즘을 사용하고, 그 외의 변경은 각 버킷의 첫 번째 노드를 기준으로 부분적인 잠금을 획득하여 동시성을 보장합니다.

 

CAS이란 Compare And Swap의 약자로 현재 쓰레드에 저장된 값과 메인 메모리에 저장된 값을 비교하여 같다면 새로운 값으로 교체하고(true), 다르다면 재시도를 수행하는(false) 알고리즘입니다.

성공할 때 까지 무한 루프를 돌긴 하지만 멀티 스레드 환경에서 스레드들의 상태가 반복적으로 blocked<->running 되는 것보다는 성능 면에서 좋습니다.

자바에서는 volatile이라는 키워드로 메인 메모리에 접근할 수 있습니다.

CAS를 통해 synchronized가 blocking하는 단점을 해결하여 non-blocking 방식으로 동시성을 보장할 수 있습니다.

참고로 synchronized 블록의 경우에는 synchronized 블록 진입전 후에 메인 메모리와 CPU 캐시 메모리의 값을 동기화하는 방식으로 원자성을 보장합니다.

 

출처

https://github.com/castello/javajungsuk_basic/blob/master/javajungsuk_basic_%EC%9A%94%EC%95%BD%EC%A7%91.pdf

https://palpit.tistory.com/entry/Java-%EC%BB%AC%EB%A0%89%EC%85%98-%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC-%EB%8F%99%EA%B8%B0%ED%99%94-%EB%B3%91%EB%A0%AC-%EC%B2%98%EB%A6%AC

https://steady-coding.tistory.com/568

https://steady-coding.tistory.com/575

https://javaplant.tistory.com/23