java, thread-safe로 바라보는 LocalThread와 ConcurrentHashMap
thread safety 란?
- 병렬 처리를 학습하며 thread safety 혹은 thread-safe란 표현을 자주 본다. 병렬 처리 과정에서 안전하다는 의미이다.
- 한편, 나는 안전하고 효과적인 병렬 처리를 위해 자주 사용되는 LocalThread과 ConcurrentHashMap을 학습하며, thread safety가 무엇인지 혼란스러웠다.
- LocalThread은 스레드 마다 별도로 할당된 공간으로서, 다른 스레드의 ThreadLocal에는 접근할 수 없다.
- ConcurrentHashMap은 여러 스레드가 동시에 접근 가능하되, thread safety를 보장한다.
- 두 객체가 스레드에 제공하는 기능과 용도는 명확하게 달라 보인다. thread safety란 정확히 무엇인가?
스레드 별로 할당된 메모리와 스레드 간 함께 사용하는 메모리
-
실제로 LocalThread와 ConcurrentHashMap가 어떻게 동작하는지를 확인하자.
- ThreadLocal은 set과 put만을 가진 단순한 래핑 클래스이다.
- 두 개의 스레드가 동시에 ThreadLocal에 접근한다. 값을 거의 같은 시간에 삽입하고 100밀이 지난 후 거의 같은 시간에 출력한다.
- ThreadLocal은 각각의 스레드가 삽입한 값 그대로 리턴한다.
ThreadLocal<String> threadLocal = new ThreadLocal<>();
new Thread(() -> {
threadLocal.set("threadA value");
sleep(100);
log.info("=> {}", threadLocal.get());
}, "threadA").start();
new Thread(() -> {
threadLocal.set("threadB value");
sleep(100);
log.info("=> {}", threadLocal.get());
}, "threadB").start();
// 23:13:15.410 [threadA] INFO datastructure.ThreadAccessData - => threadA value
// 23:13:15.410 [threadB] INFO datastructure.ThreadAccessData - => threadB value
- 이와 반대로 ConcurrentHashMap은 스레드 두 개 모두 “threadA value” 혹은 “threadB value”만 리턴함을 확인할 수 있다.
ConcurrentHashMap<String, String> concurrentHashMap = new ConcurrentHashMap<>();
new Thread(() -> {
concurrentHashMap.put("key", "threadA value");
sleep(100);
log.info("=> {}", concurrentHashMap.get("key"));
}, "threadA").start();
new Thread(() -> {
concurrentHashMap.put("key", "threadB value");
sleep(100);
log.info("=> {}", concurrentHashMap.get("key"));
}, "threadB").start();
// 23:12:03.308 [threadB] INFO datastructure.ThreadAccessData - => threadB value
// 23:12:03.308 [threadA] INFO datastructure.ThreadAccessData - => threadB value
sleep(150);
log.info("concurrentHashMap.size() : {}", concurrentHashMap.size());
// 23:16:15.731 [Test worker] INFO datastructure.ThreadAccessData - concurrentHashMap.size() : 1
- ThreadLocal의 경우 각각의 스레드마다 할당된 별도의 공간이 있으므로 병렬 처리에 있어서 완전하게 안전해 보인다.
- 반대로 ConcurrentHashMap는 두 개의 스레드가 동시에 접근하여 같은 key를 가진 서로 다른 값을 가진 value가 삽입 가능하며 둘 중 하나는 제거 된다. 이렇게 데이터를 엉키게 만드는 ConcurrentHashMap는 왜 thread safety 한가?
내부의 로직이 정상적으로 처리되는 의미에서의 thread safety와 Synchronized
- 사실 ThreadLocal의 메모리가 분리되어 있다는 의미로 thread safety가 사용되지 않는다. 위키 백과에 따르면 thread safety은 다음과 같다.
- 스레드 안전(thread 安全, 영어: thread safety)은 멀티 스레드 프로그래밍에서 일반적으로 어떤 함수나 변수, 혹은 객체가 여러 스레드로부터 동시에 접근이 이루어져도 프로그램의 실행에 문제가 없음을 뜻한다. 보다 엄밀하게는 하나의 함수가 한 스레드로부터 호출되어 실행 중일 때, 다른 스레드가 그 함수를 호출하여 동시에 함께 실행되더라도 각 스레드에서의 함수의 수행 결과가 올바로 나오는 것으로 정의한다.
- 재진입성: 어떤 함수가 한 스레드에 의해 호출되어 실행 중일 때, 다른 스레드가 그 함수를 호출하더라도 그 결과가 각각에게 올바로 주어져야 한다.
- 스레드 안전의 특징 중 하나인 재진입성을 보자. 한 함수에 여러 스레드가 접근 하더라도 각각의 스레드에 올바른 결과를 전달하는 것에 초점을 맞춰야 한다.
- 재진입성을 보장하기 위한 자바의 기능 중 하나는 키워드 Synchronized 이다. Synchronized 가 선언된 메서드, 블럭은 단 하나의 스레드만 접근 가능하다.
- 이를 잘 보여주는 자료구조 Vector의 add 메서드를 예를 들어 확인해보자.
public synchronized boolean add(E e) {
modCount++;
add(e, elementData, elementCount);
return true;
}
- Vector의 경우 add 메서드 전체가 synchronized로 선언되어 있다. 이 말은 add 메서드에 진입 가능한 스레드가 단 하나라는 의미이다. 왜 add 메서드는 단 하나의 스레드만 사용하는 것을 보장하였을까?
- add 메서드 내부에는 데이터를 다루는 알고리즘과 로직으로 이뤄져 있다. 어떤 로직은 여러 스레드가 접근해도 문제가 없지만 어떤 로직은 여러 스레드가 접근할 경우 로직 자체가 망가지는 경우가 있다. 그러므로 Vector는 병렬 처리 과정에서 add 메서드를 하나의 스레드만 접근 가능하게 만들어 내부 로직의 정상적인 동작을 보장한다는 의미로서 thread safety 하다.
- ArrayList는 Vector와 달리 synchronized가 없다. 이 말은 한 객체의 add 메서드에 여러 스레드가 접근하면 로직 자체가 망가질 수 있다는 의미이다.
- ConcurrentHashMap은 Vector보다 진보된 자료구조이다. Vector와 달리 부분에 대해서만 synchronized로 잠근다. 이를 통하여 병렬 처리에서의 안정성을 보장하며 동시에 더 빠른 성능을 제공한다.
Thread safe의 다양한 의미
- 결과적으로 ThreadLocal의 Thread-Safe와 ConcurrentHashMap 혹은 Vector의 Thread-Safe의 의미는 다르다.
- 전자는 Thread 마다 새로운 자료구조가 존재함을 의미하며, 완전하게 분리되어 있기 때문에 Thread-safe하다.
- 후자는 같은 자료구조를 쓰레드 간 함께 쓸 수 있다. 이로 인하여 의도치 않는 데이터의 손실이나 변경 문제가 발생할 수 있다. 이런 입장으로 본다면 ConcurrentHashMap은 병렬 처리로부터 안전하지 않다. 하지만 ConcurrentHashMap는 병렬처리로부터 안전한데, 해당 객체의 내부 로직은 여러 스레드가 접근하더라도 재진입성을 보장한다.
- 우리는 동일한 데이터베이스에 여러 개의 어플리케이션이나 스레드가 접근한다고 하여 문제가 있다고 말하지 않는다. 그보다는 격리수준과 락을 적절하게 사용하여 하나의 트랜잭션이 의도하는 바에 따라 DB를 조회하고 갱신하는 것에 우리는 초점을 맞춘다.
참고
- https://www.inflearn.com/questions/347336
- https://cornswrold.tistory.com/209