하나의 자원에 여러 프로세스 또는 스레드가 사용하려고 할 때. 사용되는 영역을 임계영역이라 하고, 자원을 점유하기 위해 여러 동시성제어를 할 수 있는 도구를 사용한다.
그럼 여러 동시성제어를 할 수 있는 도구란 무엇을 말하는가?
개념적으로는 뮤텍스, 세마포어, 모니터 가 있을 것이고
실제 코드는 `java.util.concurrent` 패키지에 담겨있다.
그 외 실행자 프레임워크, 동기화 장치(synchronizer) 등이 존재합니다.
Java에서는 실행자 프레임워크를 통해서 작업하길 권고하고 있다. [이펙티브 자바 Item81]
그 이유는 위에서 언급한 뮤텍스, 세마포어, 모니터를 조작하기 위해서 wait, notify와 같은 코드를 작성해 동시성을 제어해야 하는데, 이때 복잡성이 더 커지기 때문이다.
리소스 자원을 자료구조가 점유하고 있을 때, 멀티 스레드 환경에서 발생한다.
대표적으로, 동시성 문제가 일어나는 코드는 Cache와 연관성이 높은것 같다.
유지보수하는 사내 서비스에서는 Shiro를 활용해 사용자인증이 이루어진다.
이때 특정 사용자의 정보도 shiro를 통해 가져오는데- 이 때 계정의 Properties를 HashMap으로 설정했을 때 당시에는 몰랐지만- 시간이 지나서 사용자의 비밀번호 초기화등과 같은 여러 스레드가 세션정보를 변경하는 처리에서 동시성 문제가 발생했다.
간단한 테스트 코드를 아래에 첨부한다.
import java.util.Map;
public class MapTestTask implements Runnable {
private Map hashMap;
private Object value = new Object();
public MapTestTask(Map map) {
this.hashMap = map;
}
public void run() {
hashMap.put(Thread.currentThread(), value);
Object retrieved = hashMap.get(Thread.currentThread());
if (retrieved == null) {
System.out.println("Oh My God it can happen.");
}
}
}
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.junit.jupiter.api.Test;
public class HasMapCurrencyTest {
@Test
void test() throws InterruptedException {
Map map = new HashMap();
// Map map = new ConcurrentHashMap();
// Map m = Collections.synchronizedMap(map);
int NUM_THREADS = 1000;
Thread[] threads = new Thread[NUM_THREADS];
for (int i = 0; i < NUM_THREADS; i++) {
threads[i] = new Thread(new MapTestTask(map));
}
for (int i = 0; i < NUM_THREADS; i++) {
threads[i].start();
}
for (int i = 0; i < NUM_THREADS; i++) {
threads[i].join();
}
}
}
null이 발생하는 동시성 문제가 발생한다.
이를 해결하기 위해서는 ConcurrentHashMap 을 선언하거나, Collections.synchronizedMap 를 활용해야 한다.
이 둘의 차이점은 무엇일까?
동기화된 맵을 사용하는가? 동시성 맵을 사용하는냐?
둘이 굉장히 비슷한 말인것 같은데.... 무엇이 다를까?
Collections.synchronizedMap 의 경우에는
자료구조 안에서 Mutex를 가지고 있어, 데이터를 put, get할 때 Mutex 를 통해 동기화를 합니다. 코드에서 synchronized가 되어 있는 것을 발견할 수 있습니다. 이는 멀티스레드 환경에서 동시성을 보장할 수 있지만, 성능을 보장하기 힘듭니다.
그래서 HashMap 의 단점을 보안하고자 SynchronizedMap이 나온것으로 보입니다.
그렇다면 ConcurrentHashMap은 왜 쓰는 것일까?
속도도 보장하면서, 동시성을 보장할 수 있는 자료구조
ConcurrentHashMap 은 Map 전체에 동기화 락을 거는 것이 아니라, Map을 여러 조각으로 쪼개어 부분부분 락을 거는 형태로 구현되어 있습니다.
또한 ConcurrentHashMap에서 Get을 할 경우네는 value가 volatile 로 구현된 Node를 활용하기 때문에 가장 최근의 값만 보여줍니다. 그래서 좀 더 빠른 성능을 보여줍니다. 그래서 get() 메서드에 좀 더 나은 성능을 보여주지만, putIfAbsent()를 할 때는 아래와 같은 코드로 짜는 것이 좋습니다.
import java.util.concurrent.*;
// ConcurrentMap으로 구현한 동시성 정규화 맵
public class Intern {
// 코드 81-1 ConcurrentMap으로 구현한 동시성 정규화 맵 - 최적은 아니다. (432쪽)
private static final ConcurrentMap<String, String> map =
new ConcurrentHashMap<>();
// public static String intern(String s) {
// String previousValue = map.putIfAbsent(s, s);
// return previousValue == null ? s : previousValue;
// }
// 코드 81-2 ConcurrentMap으로 구현한 동시성 정규화 맵 - 더 빠르다! (432쪽)
public static String intern(String s) {
String result = map.get(s);
if (result == null) {
result = map.putIfAbsent(s, s);
if (result == null)
result = s;
}
return result;
}
}
[참고자료]
'가치관 쌓기 > 개발 돌아보기' 카테고리의 다른 글
우아한테크코스Pro 프리코스 후기 (1) | 2021.05.17 |
---|---|
빠르게 실패하기(fail-fast) VS 안전하게 실패하기(fail-safe) (4) | 2021.04.16 |
JPA 연관 관계를 명시(사용)하는 이유는 무엇일까? (0) | 2021.01.16 |
테스트 주도 설계를 실천한다는 것 (0) | 2020.12.18 |
Hibernate, JPA, Querydsl는 과연 좋은 도구일까? (10) | 2020.10.16 |
댓글