실행 중인 Thread 를 중지하는 API 가 Deprecated 되었다. 하지만 여전히 Thread 를 중지 시키려는
수요가 있기 때문에 이에 대해 공부해 보았다.
실행 중인 Thread 를 중지 시키는 방법을 살펴보면 크게 두가지가 있다.
1. Flag 변수를 사용하기
2. Interrupt 이용하기
이 중 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 마다 가지고 있는 cache 에 running 이라는 flag 값이 저장되었기 때문이다.
Thread 고유의 Cache 에 저장하는 많은 요인들이 있지만, 컴파일러 최적화나 CPU 아키텍처 및 캐시 정책에 따라 기준이 상이하다.
주로 자주 사용되는 변수들의 경우 Thread 의 Thread Local Cache에 저장된다.
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 변수를 보다 안전하게 사용할 수 있다.
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();
}
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변수를 사용할 수 있는 것이 신기했다.
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 나 언어에서 그런것은 아니지만 말이다..
메모리 장벽은 다른 스레드가 변수 변경 사실을 인지하도록 보장하는 메커니즘이다.
첫번째 스레드에서 이후 변수가 변경된 것을 감지할 수 있다.
결론적으로 메모리 장벽이 보장되지 않으면,
스레드 로컬 캐시에 저장되는 변수 값이 다른 스레드로 인해 변경된 변수 값을 인지하지 못하기 때문에
멀티 스레드 환경에서는 메모리 가시성이 보장되는 변수나 메커니즘을 사용해야한다.