일반적으로 사용하는 HashMap 의 경우 동시성을 보장해주지 않아서 멀티 스레드 환경에서 의도한대로 동작하지 않는다.
이전에 HashMap의 동작 방식에 대해 학습을 해봤는데 간단하게 정리하면 HashMap은 key 값으로 사용하는 객체의 hashCode()를 활용해서 배열(버킷)의 index를 구한다. 하지만 버킷의 크기는 제한적이고 동적으로 확장 시키면서 사용하기 때문에 다른 key, 같은 index를 가지는 경우가 생기고 이런 경우 자바에서는 링크드 리스트 또는 트리 노드 형태로 연결(Chaining) 시키면서 값을 저장한다.
다음은 HashMap을 사용하고 동시에 여러 스레드에서 User를 저장하는 코드이다. save() 메서드는 id 값을 늘리고 user에 담아서 HashMap에 저장하는 간단한 동작이다.
테스트 코드는 ThreadPool을 여러개 생성하고 스레드별로 for문을 돌리면서 save()를 호출하도록 한다. CountDownLatch는 스레드가 종료될때마다 값을 countDown()으로 내리면서 0이 될때까지 대기(await)를 하는데 Main 스레드가 동작하는 것을 막기 위해 사용했다.
리뷰어님한테 동시성 테스트의 경우 확률에 따라 영향을 받는다는 점을 유의하라는 피드백을 받고 @RepeatedTest를 통해 테스트를 반복해봤는데 여러번 돌려보니까 첫번째는 항상 실패를 하고 이후부터는 거의 다 성공을 한다.
뭔가 이상해서 테스트 수행 시간을 보니 첫번째 테스트를 하기 전에 초기화(?) 작업들을 하면서 시간이 오래 걸리고 그 영향으로 계속 실패를 하는 것 같았다. 실제로는 테스트 케이스가 너무 적어서 동시성 문제가 거의 발생하지 않는 상황인 것이다. 그래서 테스트 케이스를 확 늘려보니 의도한 결과가 나왔다. 동시성 테스트를 할 때는 반복 횟수와 테스트 케이스를 높게 주는 것을 신경 써야겠다.
자바는 HashMap의 동시성 문제를 해결하기 위해 ConcurrentHashMap(java.util.concurrent)을 제공하는데 ConcurrentHashMap으로 바꿔봐도 테스트는 여전히 실패한다. ConcurrentHashMap의 key 값으로 사용하는 Long 타입의 값을 증가시키는 과정에서 동시성 문제가 발생하기 때문이다. 자바는 언어 명세상 long, double을 제외한 변수를 읽고 쓰는 것이 원자적으로 수정이 완전히 반영된 값을 읽어오지만 한 스레드가 변경한 값이 다른 스레드에서 보이는가를 보장하지 않는다. 그래서 한 스레드가 변경한 값이 반영되기 전에 다른 스레드가 값을 읽어온 상태면 같은 id 값을 가지는 문제가 생긴다. (이펙티브 자바)
AtomicLong(java.util.concurrent)을 사용하면 이런 문제를 해결할 수 있다. 스레드는 메인 메모리에서 값을 읽어와서 CPU 캐시 메모리에 저장을 하는데 volatile 키워드를 사용하면 메인 메모리에서 직접 값을 읽어올 수 있다.
AtomicLong은 CAS(Compare-And-Swap) 알고리즘을 활용한 lock free 방식으로 동작하는데 내부적으로 volatile을 사용해서 스레드에 저장된 값과 메인 메모리의 값을 비교하고 같으면 변경하는 식으로 동작한다. 만약 값을 비교해서 다르면 재시도를 하기 때문에 synchronized처럼 락을 걸 필요가 없다. 다시 테스트를 해보니 정상적으로 통과가 되었다.
ConcurrentHashMap은 CAS와 부분적으로 lock을 사용해서 동시성을 보장하는데 빈 버킷에 값을 저장할때는 CAS를 사용하고 버킷에 노드가 존재하는 경우 블록 단위로 synchronized를 사용해서 값을 저장한다. read를 할 때는 락을 걸지 않기 때문에 성능도 준수한 편이다.
멀티 스레드 환경에서는 공유 자원으로 인해 발생하는 문제를 조심해야 하는데 상황에 따라 ThreadLocal을 활용하는 것도 좋을 것 같다. (ThreadPool의 경우 스레드를 재사용하기 때문에 초기화에 신경써야 한다.)
[참고]
https://www.geeksforgeeks.org/concurrenthashmap-in-java/
'Java' 카테고리의 다른 글
[Java] 자바로 간단한 웹서버 구현 2 (0) | 2023.06.08 |
---|---|
[Java] 자바로 간단한 웹서버 구현 1 (0) | 2023.06.03 |
[Java] HashMap 구조 (0) | 2023.03.27 |
[Java] System.in 테스트 하는 방법 (0) | 2023.03.11 |
[Java] static import 주의점 (0) | 2022.10.13 |