Thread Local Cache ..?

XingXi·2024년 6월 11일
0

기록

목록 보기
27/33

Thread 중지하기

실행 중인 Thread 를 중지하는 API 가 Deprecated 되었다. 하지만 여전히 Thread 를 중지 시키려는
수요가 있기 때문에 이에 대해 공부해 보았다.


크게 두가지

실행 중인 Thread 를 중지 시키는 방법을 살펴보면 크게 두가지가 있다.


1. Flag 변수를 사용하기
2. Interrupt 이용하기


이 중 Flag 변수를 사용하는 방법에 대해서 알아보자.


Flag 변수를 다음과 같이 사용한다면 ..

flag 변수는 거창한 것이 아니라, 단지 변화를 감지하기 위해서 사용한다.
변화를 감지하기 위해서는 다음과 같이 사용할 수 있다.

	private static boolean running = true;
    
    public static void main(String[] args) {

        new Thread(()->{
            int count = 0 ;
            while(running) {
                count++;
            }
            System.out.println("First Thread Running Over");
        }).start();


        new Thread(()->{
            // ** Thread Sleep 을 하는 이유는 위의 Thread 에서 반복 문을 진행하기 전에
            // ** running 변수가 false 로 변하는 것을 막기 위함이다.
            try{
                Thread.sleep(1000);

            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            running = false;
            System.out.println("Second Thread Running Over");
        }).start();
    } 

하지만 이렇게 사용하면 다음과 같은 결과를 얻는다.

두번째 실행한 Thread가 실행이 되고 첫번째 Thread 의 loof 에서 빠져 나오지 못하고 있는 모습이다.
두번째 Thread 의

running = false;

이 부분에서 flag 로 사용되고 있는 running 값이 false 가 되었음으로

while(running) {

첫번째 Thread 의 반복문이 종료가 되어야 하는데 그렇지 않았다. 왜 그런 것일까?


🚀 Thread Local Cache

원인은 바로 해당 변수를 메모리 영역에 저장하는 것이 아닌, Thread 마다 가지고 있는 cacherunning 이라는 flag 값이 저장되었기 때문이다.


Thread 고유의 Cache 에 저장하는 많은 요인들이 있지만, 컴파일러 최적화나 CPU 아키텍처 및 캐시 정책에 따라 기준이 상이하다.

주로 자주 사용되는 변수들의 경우 Thread 의 Thread Local Cache에 저장된다.


이를 해결하려면

Volatile 선언

    private volatile static boolean running = true;

    public static void main(String[] args) {

        new Thread(()->{
            int count = 0 ;
            while(running) {
                count++;
            }
            System.out.println("First Thread Running Over");
        }).start();


        new Thread(()->{
            // ** Thread Sleep 을 하는 이유는 위의 Thread 에서 반복 문을 진행하기 전에
            // ** running 변수가 false 로 변하는 것을 막기 위함이다.
            try{
                Thread.sleep(1000);

            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            running = false;
            System.out.println("Second Thread Running Over");
        }).start();
    }

다음과 같이 Voltile 을 선언해서 사용하면 Thread Local Cache 가 아닌 메모리에 변수를 저장하여 Flag 변수를 보다 안전하게 사용할 수 있다.

Atomic 객체 선언

    private static AtomicBoolean running = new AtomicBoolean(true);

    public static void main(String[] args) {

        new Thread(()->{
            int count = 0 ;
            while(running.get()) {
                count++;
            }
            System.out.println("First Thread Running Over! Atomic Count : "+count);
        }).start();


        new Thread(()->{
            // ** Thread Sleep 을 하는 이유는 위의 Thread 에서 반복 문을 진행하기 전에
            // ** running 변수가 false 로 변하는 것을 막기 위함이다.
            try{
                Thread.sleep(1000);

            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            running.set(false);
            System.out.println("Second Thread Running Over");
        }).start();
    }

? Thread.sleep...?

    private static boolean running = true;

    public static void main(String[] args) {

        new Thread(()->{
            int count = 0 ;
            while(running) {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                count++;
            }
            System.out.println("First Thread Running Over! COUNT : "+count);
        }).start();


        new Thread(()->{
            // ** Thread Sleep 을 하는 이유는 위의 Thread 에서 반복 문을 진행하기 전에
            // ** running 변수가 false 로 변하는 것을 막기 위함이다.
            try{
                Thread.sleep(1000);

            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            running = false;
            System.out.println("Second Thread Running Over");
        }).start();
    }

Atomic 도 voltile 도 아닌 Thread Local Cache 에 저장되었던 것 처럼 변수를 선언했는데,
고작 Thread sleep 하나 걸어줬다고 FLAG변수를 사용할 수 있는 것이 신기했다.


사실 Thread.sleep 을 쓰면 ..

    private static boolean running = true;

    public static void main(String[] args) {

        new Thread(()->{
            int count = 0 ;
            while(running) {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                count++;
            }
            System.out.println("First Thread Running Over! COUNT : "+count);
        }).start();


        new Thread(()->{
            // ** Thread Sleep 을 하는 이유는 위의 Thread 에서 반복 문을 진행하기 전에
            // ** running 변수가 false 로 변하는 것을 막기 위함이다.
            try{
                Thread.sleep(1000);

            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            running = false;
            System.out.println("Second Thread Running Over");
        }).start();
    }

여기서 일어나는 일은 다음과 같다.


1. Thread1 내부에서 sleep 이 발생, Thread 1 이 대기 상태가 되며
Thread 1에서 할당된 변수들을 Thread Local Cache 에 저장하고 초기화 된다.
2. Thread2 가 flag 변수를 false 로 변경하고, Thread 2 의 작업이 종료 된다.
3. 다시 Thread1 로 다시 컨텍스트 스위칭이 일어나며, 이때 Thread 2에서 변경한 flag 값을 Thread 1 의 flag 값에 할당하여, Thread 1 의 반복문이 종료된다.


그럴 싸해 보이지만, 그렇게 치면 Thread.sleep 이 없는 경우도 마찬가지 아닌가?


메모리 장벽?

여기서 Thread.sleep()은 메모리 장벽 역할을 수행한다. 모든 OS 나 언어에서 그런것은 아니지만 말이다..
메모리 장벽은 다른 스레드가 변수 변경 사실을 인지하도록 보장하는 메커니즘이다.
첫번째 스레드에서 이후 변수가 변경된 것을 감지할 수 있다.

🚀 결론적으로

결론적으로 메모리 장벽이 보장되지 않으면,
스레드 로컬 캐시에 저장되는 변수 값이 다른 스레드로 인해 변경된 변수 값을 인지하지 못하기 때문에
멀티 스레드 환경에서는 메모리 가시성이 보장되는 변수나 메커니즘을 사용해야한다.

내가 공부한건 여기까지..

0개의 댓글