동시성 문제와 해결방법

동시성 문제가 발생하는 이유는

동일한 자원에 여러 스레드가 동시에 접근했을때 발생하는 문제이다.

이러한 동시성 문제는 지역 변수에서는 발생하지 않는다.

지역 변수는 스레드마다 각각 다른 메모리 영역이 할당되기 때문입니다.

동시성 문제가 발생하는 곳은 같은 인스턴스 필드(주로 싱글톤)

또는 static 같은 공용 필드에 접근할 때 발생한다.

여기서 중요한 점은 값에 무조건 동시에 접근한다고

문제가 발생하는 것이 아니라 값을 어디선가 변경할 때 발생!

즉, 읽기만 한다면 동시성 문제는 발생하지 않는다.

동시성 문제 해결하기 위해서 스레드 로컬을 사용한다.

스레드 로컬은 해당 스레드만 접근할 수 있는 개인 저장소를 의미한다.

ThreadLocal을 모두 사용하고 나면 반드시 ThreadLocal.remove() 를 호출해서

쓰레드 로컬에 저장된 값을 제거해야 한다.


동시성 문제란?

동일한 자원에 여러 스레드가 동시에 접근했을때 발생하는 문제이다.

@Slf4j
public class FieldService {

    private String nameStore;

    public void logic(String name){
        nameStore.set(name);
        log.info("저장 name={} -> nameStore={}",name,nameStore.get());
        sleep(1000);
        log.info("조회 nameStore ={}",nameStore.get());        
    }

    private void sleep(int millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

FieldService 클래스의 logic 메서드는 인자로 들어온 name값을 nameStore 필드에 저장하고

1 쉬고 저장된 nameStore값을 조회하는 메서드이다.


@Slf4j
public class FieldServiceTest {

    private FieldService fieldService = new FieldService();

    @Test
    void field(){
        log.info("main start");

        Thread threadA = new Thread(() -> fieldService.logic("userA"));
        Thread threadB = new Thread(() -> fieldService.logic("userB"));

        threadA.start();
        sleep(100);
        threadB.start();

        sleep(3000);
        log.info("main exit");

    }
    private void sleep(int millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

FieldService의 logic 메서드를 2개의 쓰레드가 0.1 간격으로 접근하는 테스트
동시성 문제를 고려하지 않았다면

main start
저장 name=userA -> nameStore=userA
조회 nameStore =userA
저장 name=userB -> nameStore=userB
조회 nameStore =userB
main exit

이런식으로 찍히길 기대하고 있지만, 실제로는
동시성 문제가 발생하여 

main start
저장 name=userA -> nameStore=userA
저장 name=userB -> nameStore=userB
조회 nameStore =userB
조회 nameStore =userB
main exit
 로그가 찍히게 됩니다.

실제로 로직은 다음과 같이 동작합니다.

1. A쓰레드가 logic에 들어와 nameStore값을 userA로 세팅하고 1 대기상태로 들어간다.
2. B쓰레드가 logic에 들어와 nameStore값을 userB로 세팅하고 1 대기상태로 들어간다.
3. A쓰레드가 깨어나서 nameStore값을 읽어서 조회하는데 이때, 
4. nameStore에는 자신이 저장한 값이 아닌 B쓰레드가 변경한 값이 들어있다.
5. B쓰레드가 깨어나서 nameStore값을 읽는다.

이러한 동시성 문제는 지역 변수에서는 발생하지 않는다.
지역 변수는 스레드마다 각각 다른 메모리 영역이 할당되기 때문입니다.

동시성 문제가 발생하는 곳은 같은 인스턴스 필드(주로 싱글톤) 또는 static 같은 공용 필드에 접근할  발생한다.

여기서 중요한 점은 값에 무조건 동시에 접근한다고 문제가 발생하는 것이 아니라 값을 어디선가 변경할  발생!
, 읽기만 한다면 동시성 문제는 발생하지 않는다.

동시성 문제 해결 -> ThreadLocal Permalink

스레드 로컬은 해당 스레드만 접근할  있는 개인 저장소를 의미한다.

하나의 자원을 보관 창구 직원, 
 개의 쓰레드를 물건 보관소 고객이라고 생각해보자.

 고객이 동시에 창구 직원에게 물건을 보관하면 
직원은  물건을 다른 보관함에 넣어두었다가  고객이 오면  보관함에서 찾아 꺼내주는 형태.

앞선 예시 코드에서 ThreadLocal를 적용해보자면,

@Slf4j
public class FieldService {

    private ThreadLocal<String> nameStore = new ThreadLocal<>();

    public void logic(String name){
        nameStore.set(name);
        log.info("저장 name={} -> nameStore={}",name,nameStore.get());
        sleep(1000);
        log.info("조회 nameStore ={}",nameStore.get());
        nameStore.remove();      
    }

    private void sleep(int millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

ThreadLocal의 사용법

1.동시에 접근하는 자원의 타입을 ThreadLocal로 변경하고 기존의 타입을 제네릭 안에 넣는다.
     - 반드시 new ThreadLocal  생성 하여 할당한다.
2. ThreadLocal.set() : 값을 저장
3. ThreadLocal.get() :  조회
4. ThreadLocal.remove() :  제거
     - 쓰레드가 사용이 끝난 후에는 반드시 remove로 값을 제거해야 한다.

코드를 위와 같이 수정하고 앞서 작성했던 FieldServiceTest를 돌려보면,

main start
저장 name=userA -> nameStore=userA
저장 name=userB -> nameStore=userB
조회 nameStore =userA
조회 nameStore =userB
main exit

동시성 문제 해결!

주의사항!!

ThreadLocal.remove()  호출해서 스레드 로컬에 저장된 값을 제거해야한다.