1. Hashtable, HashMap, ConcurrentHashMap
위에 나열된 클래스들은 Map 인터페이스를 구현한 콜렉션들입니다. 이 콜렉션들은 비슷한 역할을 하는것 같으면서도 다르게 구현되어 있습니다. 기본적으로 Map 인터페이스를 구축한다면 <key, value>구조를 가지게 됩니다. 하나씩 살펴봅시다.
2. Hashtable
Hashtable은 put, get과 같은 주요 메소드에 synchronized 키워드가 선언 되어 있습니다. 또한 key, value에 null을 허용하지 않습니다.
3. HashMap
HashMap은 주요 메소드에 synchronized 키워드가 없습니다. 또한 Hashtable과 다르게 key, value에 null을 입력할 수 있습니다.
하지만 HashMap도
"Map<String,Integer> map = Collections.synchronizedMap(new HashMap<String,Integer>());"
와 같이 선언하면 Thread-safe한 맵으로 사용가능하다.
4. ConcurrentHashMap
HashMap을 thread-safe 하도록 만든 클래스가 ConcurrentHashMap입니다. 하지만 HashMap과는 다르게 key, value에 null을 허용하지 않습니다. 또한 putIfAbsent라는 메소드를 가지고 있습니다.
5. Common Methods
위의 세종류의 클래스들은 put, get 메소드 외에도 기본적인 메소드들을 구현하고 있습니다. 대표적인 몇가지의 메소드들만 알아봅시다.
- clear()
해당 콜렉션의 데이터를 초기화 합니다.
- containsKey(key)
해당 콜렉션에 입력 받은 key를 가지고 있는지 체크합니다.
- containsValue(value)
해당 콜렉션에 입력 받은 value를 가지고 있는지 체크합니다.
- remove(key)
해당 콜렉션에 입력 받은 key의 데이터(key도 포함)를 제거합니다.
- isEmpty()
해당 콜렉션이 비어 있는지 체크합니다.
- size()
해당 콜렉션의 엔트리(Entry) 또는 세그먼트(Segment) 사이즈를 반환합니다.
6. In Multi Threads(ConcurrentModificationException...)
HashMap에 대한 부분은 동기화가 이루어지지 않습니다. 하지만 HashMap을 쓰더라도 synchronized 블록을 선언해 주면 정상으로 동작을 합니다. 따라서 동기화 이슈가 있다면 일반적인 HashMap을 쓰지 말거나 쓰더라도 동기화를 보장하는 HashMap 콜렉션 또는 synchronized 키워드를 이용해 동기화 처리를 반드시 해주는 것이 좋아보입니다. 혹은 Thread-safe한 ConcurrentHashMap을 쓰시는 것을 권장합니다.
만약 하나의 스레드가 Map에 접근하여 요소들을 삭제,수정,삽입 등을 작업하고 있는 도중에 다른 스레드가 해당 Map에 접근 해 무엇인가를 작업한다면 동기화 문제가 발생할 수 있습니다.
밑의 소스에서 일반 HashMap을 사용한다면 위에서 말한 예외가 발생할 경우가 생깁니다. 이 경우를 ConcurrentHashMap을 사용해 Thread-safe한 코드로 변경하였습니다.
@Service
public class SessionService {
private static final Logger log = LoggerFactory.getLogger(SessionService.class);
/*Map<String, SessionInfo> sessionMap = new HashMap<>();*/
/*
*
* 설명 : HashMap을 썼을 경우, ConcurrentModificationException 발생(Thread간의 동기화문제)
* HashMap -> ConcurrentHashMap
*/
Map<String, SessionInfo> sessionMap = new ConcurrentHashMap<>();
Boolean runFlag = true;
private Thread itsThread = null;
@Value("${app.session.expire.sec}")
private int expiredSec;
@PostConstruct
private void dropExptiredSession() {
itsThread = new Thread(() -> {
try {
while (runFlag) {
/*
* yeoseong_yoon
* 설명 : 바로 밑에 메소드 주석 풀면 10초마다 스레드가 돌아가면서 세션만료시간이 된
* 채팅세션을 remove한다.
*/
checkExpiredSession();
Thread.sleep(10000);
}
} catch (InterruptedException e) {
log.warn("thread interrupt occured", e);
}
});
itsThread.start();
}
@PreDestroy
private void stop() {
runFlag = false;
itsThread.interrupt();
}
private void checkExpiredSession() {
for (Map.Entry entry : sessionMap.entrySet()) {
String sessionKey = (String) entry.getKey();
SessionInfo sessionInfo = (SessionInfo) entry.getValue();
long duration = TimeUtils.getCurrentSec() - sessionInfo.getLastTimeSec();
if (duration > expiredSec) {
sessionMap.remove(sessionKey);
log.info("session time out, sessionkey={}", sessionKey);
}
}
}
}