동시성 문제를 알아보기 위해 파라미터로 넘어온 name을 클래스 변수 nameStore에 저장하고, 1초간 쉰 다음 nameStore가 참조하는 값을 반환하는 서비스 코드를 다음과 같이 작성한다.
@Slf4j
public class FieldService {
private String nameStore;
public String logic(String name) {
log.info("저장 name = {} -> nameStore = {}", name, nameStore);
nameStore = name;
sleep(1000);
log.info("조회 nameStore = {}", nameStore);
return nameStore;
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
그리고 나서 동시성 문제를 테스트하기 위해 sleep(2000)을 설정해서 threadA의 실행이 끝나고 나서 threadB가 실행되도록 하는 테스트 코드를 작성한다. 이때, FieldServie의 logic메서드 내부에 sleep(1000)으로 1초를 지연하는 코드가 있다. 따라서 1초 이후에 호출하면 순서대로 실행할 수 있다.
import hello.advanced.trace.threadlocal.code.FieldService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
@Slf4j
public class FieldServiceTest {
private FieldService fieldService = new FieldService();
@Test
void field() {
log.info("main start");
Runnable userA = () -> {
fieldService.logic("userA");
};
Runnable userB = () -> fieldService.logic("userB");
Thread threadA = new Thread(userA);
Thread threadB = new Thread(userB);
threadA.setName("threadA");
threadB.setName("threadB");
threadA.start();
sleep(2000);
threadB.start();
sleep(3000);
log.info("main exit");
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
위 테스트 코드의 실행 결과는 다음과 같이 순서대로 실행된 것을 확인할 수 있다.
20:49:50.517 [Test worker] INFO hello.advanced.trace.threadlocal.FieldServiceTest -- main start
20:49:50.519 [threadA] INFO hello.advanced.trace.threadlocal.code.FieldService -- 저장 name = userA -> nameStore = null
20:49:51.526 [threadA] INFO hello.advanced.trace.threadlocal.code.FieldService -- 조회 nameStore = userA
20:49:52.520 [threadB] INFO hello.advanced.trace.threadlocal.code.FieldService -- 저장 name = userB -> nameStore = userA
20:49:53.526 [threadB] INFO hello.advanced.trace.threadlocal.code.FieldService -- 조회 nameStore = userB
20:49:55.525 [Test worker] INFO hello.advanced.trace.threadlocal.FieldServiceTest -- main exit
동시성 문제가 발생하는 코드
이번에는 sleep(100)을 설정해서 threadA의 작업이 끝나기 전에 threadB가 실행되도록 해본다.
21:00:38.021 [Test worker] INFO hello.advanced.trace.threadlocal.FieldServiceTest -- main start
21:00:38.023 [threadA] INFO hello.advanced.trace.threadlocal.code.FieldService -- 저장 name = userA -> nameStore = null
21:00:38.128 [threadB] INFO hello.advanced.trace.threadlocal.code.FieldService -- 저장 name = userB -> nameStore = userA
21:00:39.027 [threadA] INFO hello.advanced.trace.threadlocal.code.FieldService -- 조회 nameStore = userB
21:00:39.134 [threadB] INFO hello.advanced.trace.threadlocal.code.FieldService -- 조회 nameStore = userB
21:00:41.133 [Test worker] INFO hello.advanced.trace.threadlocal.FieldServiceTest -- main exit
위의 실행 결과를 확인해보면, 결과적으로 threadA입장에서는 저장한 데이터와 조회한 데이터가 다른 문제가 발생한다. 이처럼 여러 쓰레드가 동시에 같은 인스턴스의 필드 값을 변경하면서 발생하는 문제를 동시성 문제라고 한다.
특히, 스프링 빈처럼 싱글톤 객체의 필드를 변경하여 사용할 때 이러한 동시성 문제를 조심해야 한다.
참고로 동시성 문제는 지역 변수에서는 발생하지 않는다. 왜냐하며느 지역 변수는 쓰레드마다 각각 다른 메모리 영역이 할당되기 때문이다.
그렇다면 싱글톤 객체의 필드를 사용하면서 동시성 문제를 해결하려면 어떻게 해야할까? 해결책은 ThreadLocal이다.
ThreadLocal
ThreadLocal은 해당 쓰레드만 접근할 수 있는 특별한 저장소를 말한다.
즉, ThreadLocal을 사용하면 각 쓰레드마다 별도의 내부 저장소를 제공한다. 따라서 같은 인스턴스의 ThreadLocal 필드에 접근해도 문제 없다.
자바는 언어차원에서 ThreadLocal을 지원하기 위한 java.lang.ThreadLocal 클래스를 제공한다.
ThreadLocal 사용법
특정 쓰레드가 ThreadLocal을 모두 사용하고 나면 ThreadLocal.remove() 를 호출해서 ThreadLocal에 저장된 값을 제거해주어야 한다.
- 값 저장 : ThreadLocal.set()
- 값 조회 : ThreadLocal.get()
- 값 제거 : ThreadLocal.remove()
@Slf4j
public class ThreadLocalService {
private ThreadLocal<String> nameStore = new ThreadLocal<>();
public String logic(String name) {
log.info("저장 name = {} -> nameStore = {}", name, nameStore.get());
nameStore.set(name);
sleep(1000);
log.info("조회 nameStore = {}", nameStore.get());
return nameStore.get();
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Slf4j
public class ThreadLocalServiceTest {
private ThreadLocalService service = new ThreadLocalService();
@Test
void threadLocal() {
log.info("main start");
Runnable userA = () -> service.logic("userA");
Runnable userB = () -> service.logic("userB");
Thread threadA = new Thread(userA);
Thread threadB = new Thread(userB);
threadA.setName("threadA");
threadB.setName("threadB");
threadA.start();
sleep(100);
threadB.start();
sleep(2000);
log.info("main exit");
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
21:36:31.665 [Test worker] INFO hello.advanced.trace.threadlocal.ThreadLocalServiceTest -- main start
21:36:31.667 [threadA] INFO hello.advanced.trace.threadlocal.code.ThreadLocalService -- 저장 name = userA -> nameStore = null
21:36:31.772 [threadB] INFO hello.advanced.trace.threadlocal.code.ThreadLocalService -- 저장 name = userB -> nameStore = null
21:36:32.673 [threadA] INFO hello.advanced.trace.threadlocal.code.ThreadLocalService -- 조회 nameStore = userA
21:36:32.776 [threadB] INFO hello.advanced.trace.threadlocal.code.ThreadLocalService -- 조회 nameStore = userB
21:36:33.773 [Test worker] INFO hello.advanced.trace.threadlocal.ThreadLocalServiceTest -- main exit
결과적으로 ThreadLocal 덕분에 Thread마다 각각 별도의 저장소를 가지게 된 것을 확인할 수 있다.
'Language > Java' 카테고리의 다른 글
제네릭 메서드가 필요한 이유 (0) | 2023.09.22 |
---|---|
ThreadLocal 동기화 적용 및 주의사항 (0) | 2023.09.21 |
싱글쓰레드와 멀티쓰레드, 싱글코어와 멀티코어 (0) | 2023.09.21 |
Context Switching, Thread (0) | 2023.09.20 |
I/O - 다양한 IO객체들, 객체 직렬화 (0) | 2023.09.18 |