ArrayList
와 HashMap
모두 Thread safe
하지 않다는 글을 보았다.
이를 알았으니 테스트를 통해 이를 직접 확인 하고 어떤 경우에 ArrayList 와 HashMap 등을 구현체로 쓰는 것을 지양하고
어떤 경우에는 사용해도 되는지에 대해 알아보기 위해 이 글을 작성해서 정리하려 한다.
동시성 문제란 여러 쓰레드 혹은 여러 쓰레드가 같은 자원에 접근할 때 발생할 수 있고 그 결과로 데이터 정합성이 깨질 수 있다.
쓰레드는 개념적으로 프로그램 내에서 실행되는 프로그램 제어흐름(실행단위) 을 의미한다.
한 프로세스가 여러 쓰레드를 갖는 것을 다중 쓰레드 환경 이라고 하는데 우리가 흔히 톰캣 기반으로 스프링 부트를 세팅하면 다중 쓰레드 환경이 된다.
다중 쓰레드 환경은 프로세스 처럼 작업을 동시에 처리할 수 있어서 그 효율이 좋고, 자원을 공유하기에 쓰레드간 데이터 전송에 편리한 측면이 있다.
스프링부트 에서 쓰레드는 기본적으로 유저의 요청마다 할당 된다는 개념으로 이해할 수 있다.
스프링은 프로그램 실행에 필요한 쓰레드들을 미리 생성해 놓는다. 이를 쓰레드 풀이라고 지칭한다.
max size , 작업 큐의 크기, 할당 가능한 쓰레드의 갯수(core size) 는 application.yml 파일에서 조절이 가능하다.
server:
tomcat:
threads:
max: 200 # 생성할 수 있는 thread의 총 개수
min-spare: 10 # 항상 활성화 되어있는(idle) thread의 개수
accept-count: 100 # 작업 큐의 사이즈
이렇게 말이다.
이렇게 설정을 안해주면 ServerProperties
클래스에서 Default 값에 의해 설정되고 이를 직접 확인할 수 있다.
위 Default 값은
처음에 10개의 쓰레드가 할당 되어 있는데 10개의 쓰레드가 다 차있고, 작업 큐 100개도 역시 다 차있다면 쓰레드를 새로 할당한다. 이 과정은 쓰레드가 최대 200개가 될 때까지 반복된다.
유저가 요청을 보낼 때 작업이 쓰레드에 할당된다.
쓰레드를 통해 공유된 자원에 접근할 수 있고 이를 통해 데이터 정합성에 문제가 생기는 동시성 문제가 발생할 수 있다.
public class ListService {
public static List<Integer> lists = new ArrayList<>();
public static Map<Integer,Integer> map=new HashMap<>();
}
여러 쓰레드에서 접근할 수 있는 static 한 list 와 map 을 ListService 라는 클래스에 선언한다.
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.junit.jupiter.api.Test;
class ListServiceTest {
private static final int THREAD_COUNT=2000;
@Test
void addList() throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(THREAD_COUNT);
CountDownLatch latch = new CountDownLatch(10000);
for(int i=0;i<10000;i++){
executorService.execute(() -> {
try {
ListService.lists.add(1);
} finally {
latch.countDown();
}
});
}
latch.await();
System.out.println("list의 size는 " + ListService.lists.size());
}
@Test
void addMap() throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(THREAD_COUNT);
CountDownLatch latch = new CountDownLatch(10000);
for(int i=0;i<10000;i++){
int finalI = i;
executorService.execute(()->{
try{
ListService.map.put(finalI,2*finalI);
}
finally {
latch.countDown();
}
});
}
latch.await();
System.out.println("map 의 size 는 "+ListService.map.size());
}
}
list 의 size는 정상적인 상황이라면
만 번 list 에 저장을 했기 때문에 10000개가 되어야 한다.
또한 Map의 size 역시 정상적인 상황이라면
만 번 map 에 저장을 했기 때문에 10000개가 되어야 한다.
그래서 직접 실행을 해보면
size 가 10000개가 아니라 그 보다 작은 어떤 숫자이다.
이는 ArrayList와 HashMap 이 Thread safe 하지 않기 때문이다.
이런 상황이 실제 환경이라면 요청을 했는데 그 정보가 사라질 수 있다는 의미이다.
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
public class ListService {
public static List<Integer> lists = new CopyOnWriteArrayList<>();
public static Map<Integer,Integer> map=new ConcurrentHashMap<>();
}
이제는 이와 같이 수정을 하고 테스트를 다시 돌려본다.
정상적으로 10000개가 list 와 map 모두 찍혀 있는 것을 볼 수 있다.
이는 List 에 혹은 Map에 여러 사용자가 동시에 접근할 일이 있을 때 유효한 얘기이다.
우리가 일반적으로 게시판 프로젝트를 만들때는 이에 대한 고민을 하지 않아도 되지만
만약 여러 사람에 의해 실시간으로 Map 에 무언가를 집계하는 로직이 있거나
Map에다가 Session 을 담는 WebSocket 으로 실시간 채팅 기능을 구현할 때면
이에 대한 내용을 고려해야 한다.
실제로 다른 사람들이 이해대해 고민한 내용 :
https://github.com/ytw9699/Dokky/issues/334
https://velog.io/@sihyung92/how-does-springboot-handle-multiple-requests : 스프링 부트의 쓰레드 풀