Skip to content

Commit 2c131a3

Browse files
committed
feat: ch07 정리
1 parent e98a7c9 commit 2c131a3

File tree

1 file changed

+203
-0
lines changed

1 file changed

+203
-0
lines changed

study/ch07.md

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
# 중단 및 종료
2+
작업이나 스레드를 안전하고 빠르고 안정적으로 멈추게 하는 것은 어려운 일이다. 더군다나 자바같은 경우 스레드가 작업을 실행하고 있을 때 강제로 멈추도록 하는 방법이 없다.
3+
4+
<img width="2791" height="565" alt="test" src="https://github.com/user-attachments/assets/ca6b04f5-5862-41e9-9135-fc273a5174f4" />
5+
6+
```
7+
스레드의 상태를 강제로 바꾸는 방식이 동기화 일관성을 깨뜨리고, 교착상태(Deadlock)나 데이터 손상을 유발하기 때문.
8+
```
9+
대신 `interrupt`라는 방법을 사용할 수 있게 되어 있으며 이는 특정 스레드에게 작업을 멈춰 달라고 요청하는 형태이다. 이는 실행 중이던 일을 중단할 떄 정상적인 상태에서 마무리하기 위해선 작업을 진행하던 스레드가 직접 마무리 하는 가장 적절한 방법이며, 유연성 확보에 유용하다.
10+
11+
## 작업중단
12+
실행중인 작업을 취소하고자 하는 요구 사항은 여러가지 경우에 나타난다.
13+
1. 사용자가 취소를 요청한 경우
14+
2. 시간이 제한된 작업
15+
3. 애플리케이션 이벤트
16+
4. 오류
17+
5. 종료
18+
19+
앞서 설명한 내용과 같이 자바는 스레드가 작업을 강제로 멈추도록 하는 방법이 없기에 `작업을 실행하는 스레드``취소를 요청하는 스레드`가 함께 작업을 멈추는 협력적인 방법을 사용해야만 한다.
20+
21+
작업을 쉽게 취소시킬 수 있도록 만들기 위해선 작업을 취소할때 `어떻게`, `언제`, `어떤 일`을 해야 하는지 , 이른바 `취소 정책`을 명확하게 해야한다.
22+
```java
23+
@ThreadSafe
24+
public class PrimeGenerator implements Runnable {
25+
@GuardedBy("this")
26+
private final List<BigInteger> primes
27+
= new ArrayList<BigInteger>();
28+
private volatile boolean cancelled;
29+
30+
public void run() {
31+
BigInteger p = BigInteger.ONE;
32+
while (!cancelled) {
33+
p = p.nextProbablePrime ();
34+
synchronized (this) {
35+
primes.add(p);
36+
}
37+
}
38+
}
39+
40+
public void cancel() { cancelled = true; }
41+
42+
public synchronized List<BigInteger> get() {
43+
return new ArrayList<BigInteger>(primes);
44+
}
45+
}
46+
```
47+
### 인터럽트
48+
스레드에 거는 인터럽트는 특정 스레드에게 적당한 상황이고 작업을 멈추려는 의지가 있는 상황이라면, 현재 실행 중이던 작업을 멈추고 다른 일을 할 수 있도록 해야 한다고 `신호`를 보내는 것과 같다.
49+
50+
Thread 클래스에는 `interrupt()`메소드를 활용하여 인터럽트를 걸수 있으며, `isInterrupted()` 메소드를 통해 인터럽트가 걸렸는지 확인이 가능하다.
51+
또한 스태틱으로 선언된 `interrupted()` 메소드를 호출하면 인터럽트를 해제하고, 해제하기 이전의 값이 무엇이었는지를 알려준다.
52+
53+
Thread.sleep 혹은 Object.wait 과 같은 블로킹 메소드는 인터럽트 상태를 확인하고 있다가 인터럽트가 걸리면 즉시 리턴된다.
54+
#### Java Thread interrupt() 동작 정리
55+
56+
| 스레드 상태 | interrupt() 호출 시 반응 | 깨어남 여부 |
57+
| ------------------------------------------------------------------------------------- | -------------------------------------------------- | ----------------- |
58+
| **WAITING** / **TIMED_WAITING**<br>(`sleep`, `wait`, `join`, `BlockingQueue.take` 등) | 즉시 깨어남 + `InterruptedException` 발생(플래그 false로 초기화) | ✅ 깨어남 |
59+
| **RUNNABLE**<br>(계산, 루프 등 실행 중) | 예외 없음, 플래그만 true로 설정 | ❌ 안 깸 (직접 체크해야 함) |
60+
| **BLOCKED**<br>(모니터 락 대기 중) | 즉시 안 깸, 락을 얻을 때까지 대기. 이후 플래그 true 상태로 실행 | ❌ 안 깸 |
61+
| **TERMINATED**<br>(종료됨) | 아무 일 없음 ||
62+
63+
인터럽트를 이해하고자 할 때 중요한 사항은 바로 실행 중인 스레드에 실제적인 제한을 가해 멈추도록 하는 것이 아닌 해당 스레드가 상황을 봐서 **스스로 멈춰주기를 요청**하는 것뿐이다.
64+
### 인터럽트 정책
65+
단일 작업마다 해당 작업을 멈출 수 있는 취소 정책이 있는 것처럼 스레드 역시 인터럽트 처리 정책이 있어야 한다.
66+
이는 인터럽트 요청이 들어 왔을 때 해당 스레드가 인터럽트를 어떻게 처리해야 하는지에 대한 지침이다.
67+
일반적으로 가장 범용적인 인터럽트 정책은 스레드 수준 혹은 서비스 수준에서 작업 중단 기능을 제공하는 것이다.
68+
69+
`작업(task)``스레드`가 인터럽트 상황에서 어떻게 동작해야하는지에 대한 명확한 구분도 필요하다.
70+
**작업**은 그 작업을 소유하는 스레드에서 실행되지 않고, 스레드 풀과 같이 실행만 전담하는 스레드를 빌려 사용하게 된다. 실제로 작업을 실행하는 스레드를 갖고 있지 않은 프로그램(예를 들어 스레드 풀에 작업을 넘기는 모든 클래스)은 작업을 실행하는 스레드의 인터럽트 상태를 그대로 유지해 스레드를 소유하는 프로그램이 인터럽트 상태에 직접 대응할 수 있도록 해야한다.
71+
대부분의 블로킹 메소드에서 `InterruptedException`을 던지도록 되어 있는 이유가 이때문이다.
72+
73+
그렇다고 해서 실행되던 작업이 모두 중지되어야 하는 것은 아니며, 일단 요청을 받았다는 사실만 기억하고 실행중이던 작업을 끝마친 이후 요청 받은 인터럽트에 대해 InterruptedException을 던지거나, 기타 다른 방법으로 대응할 수도 있다.
74+
75+
중요한 것은 스레드에는 해당 스레드를 소유하는 클래스에서만 인터럽트를 걸어야 하며, 그 이유는 스데르를 소유하는클래스는 shutdown과 같은 메소드에서 적절한 작업 중단 방법과 함께 인터럽트 정책을 확립해 내부적으로 적용하고 있기 때문이다.
76+
### 인터럽트에 대한 대응
77+
블로킹 메소드를 호출하는 경우에 `InterruptedException`이 발생했을 경우 처리할 수 있는 실질적인 방법에는 대략 두 가지가 있다.
78+
1. 발행한 예외를 호출 스택의 상위 메소드로 전달한다.
79+
2. 호출 스택의 상단에 위칳한 메소드가 직접 처리할 수 있도록 인터럽트 상태를 유지한다.
80+
81+
주의할 점은 catch 블록에서 InterruptedException을 잡아낸 다음 아무런 행동도 취하지 않고 예외를 먹어버리는 일은 하지 말아야한다.
82+
대부분의 프로그램 코드는 자신이 어느 스레드에서 동작할지 모르기 때문에 인터럽트 상태를 최대한 그대로 유지해야한다.
83+
84+
## 스레드 기반 서비스 중단
85+
스레드 기반의 서비스 내부에 생성되어 있는 스레드는 안전하게 종료시킬 필요가 있다. 그런데 스레드를 선점적인 방법으로 강제로 종료시킬 수 없기 때문에 스레드에게 알아서 종료해달라고 부탁할 수박에 없다.
86+
87+
즉 이는 소유권에 관한 문제로 애플리케이션이 개별 스레드에 직접 액세스하는 대신 스레드 기반 서비스가 스레드의 시작부터 종료까지의 모든 기능에 해당하는 메소드를 직접 제공해야한다.
88+
이러한 특성으로 `ExecutorService` 인터페이스는 `shutdonw()`메소드와 `shutdownNow()` 메소드를 제공하고 있다.
89+
90+
### 로그서비스(예제)
91+
[문제상황]
92+
- 로그서비스에서 로그를 발행하는 쪽과 출력하는 쪽을 `프로듀서-컨슈머 패턴`으로 구현함.
93+
- 로그를 출력하는 곳에서 문제가 생길 경우 `BlockingQueue`와 같은 블로킹 메소드를 사용해 문제가 되진 않음.
94+
- 다만 로그를 발행하는 쪽은 문제가생길 여지가 있음.
95+
96+
[해결방법]
97+
- ExecutorService를 활용하여 `shutdown()`, `shutdownNow()` 메소드를 활용함.
98+
- 강제로 종료하는 경우 응답은 훨씬 빠르지만 실행 도중에 스레드에 인터럽트를 걸어야하기에 여러 문제가 생길 수 있음.
99+
- 안전하게 종료하는 방법은 종료 속도는 느리지만 모든 작업을 처리하기 까지 기다리기에 작업을 잃을 가능성이 없어 안전함.
100+
- 독약 객체를 넣는 방법으로 특정 객체를 넣는 경우 해당 객체를 받는 경우 종료해야 한다는 의미를 가짐. 크기에 제한이 없는 큐를 사용할 때 효과적이며, 많은 수의 프로듀서와 컨슈머를 사용하는 경우에는 허술하게 보임.
101+
102+
## 비정상적인 스레드 종료 상황 처리
103+
스레드를 예상치 못하게 종료시키는 가장 큰 원인은 바로 `RuntimeException`이며 이는 대부분 프로그램이 잘못 짜여져서 발생하는 경우가 많기에 `try-catch` 구문으로 잡지 못하는 경우가 많다.
104+
해당 Exception은 호출스택을 따라 상위로 전달되기보다는 현재 실행되는 시점에서 콘솔에 스택 호출 추적 내용을 출력하고 종료하도록 되어 있다.
105+
106+
따라서 위와 같은 작업 처리 스레드는 실행할 작업을 `try-catch`구문으로 가싸 예외 상황에 대응할 수 있도록 준비하거나, `try-finally` 구문을 사용해 스레드가 피치 못할 사정으로 종료되는 경우에도 외부에 종료 된다는 사실을 알려 대응할 수 있도록 해야한다.
107+
```java
108+
public void run() {
109+
Throwable thrown = null;
110+
try {
111+
while (!isInterrupted())
112+
runTask(getTaskFromWorkQueue());
113+
} catch (Throwable e) {
114+
thrown = e;
115+
} finally {
116+
threadExited(this, thrown);
117+
}
118+
}
119+
```
120+
### 정의되지 않은 예외처리
121+
처리하지 못한 예외 상황 때문에 스레드가 종료되는 경우에 JVM이 애플리케이션에서 정의한 `UncaughtExceptionHandler`를 호출하도록 할 수 있으며, 핸들러가 정의되어 있지 않다면 기본적으로 system.err 스트림에 출력한다.
122+
```java
123+
public class UEHLogger implements Thread.UncaughtExceptionHandler {
124+
public void uncaughtException(Thread t, Throwable e) {
125+
Logger logger = Logger.getAnonymousLogger();
126+
logger.log(Level.SEVERE,
127+
"Thread terminated with exception: " + t.getName(), e);
128+
}
129+
}
130+
```
131+
스레드 풀의 작업 스레드를 대상으로 핸들러를 설정하려면 `ThreadPoolExecutor`를 생성할 때 작업용 스레드 생성을 담당하는 `ThreadFactory`클래스를 별도로 넘겨주면 된다.
132+
```java
133+
import java.util.concurrent.*;
134+
135+
public class CustomThreadFactoryExample {
136+
137+
public static void main(String[] args) {
138+
ThreadFactory customThreadFactory = new ThreadFactory() {
139+
private final ThreadFactory defaultFactory = Executors.defaultThreadFactory();
140+
private int threadCount = 1;
141+
142+
@Override
143+
public Thread newThread(Runnable r) {
144+
Thread t = defaultFactory.newThread(r);
145+
t.setName("MyWorker-" + threadCount++); // 스레드 이름 설정
146+
t.setDaemon(false); // 데몬 스레드 여부 설정
147+
t.setUncaughtExceptionHandler((thread, ex) -> {
148+
System.out.printf("[예외발생] 스레드: %s, 예외: %s%n",
149+
thread.getName(), ex.getMessage());
150+
});
151+
return t;
152+
}
153+
};
154+
155+
ExecutorService executor = new ThreadPoolExecutor(
156+
2, 4,
157+
60, TimeUnit.SECONDS,
158+
new LinkedBlockingQueue<>(),
159+
customThreadFactory
160+
);
161+
162+
// 테스트 작업 제출
163+
executor.submit(() -> {
164+
System.out.println(Thread.currentThread().getName() + " 실행 중");
165+
throw new RuntimeException("테스트 예외");
166+
});
167+
168+
executor.shutdown();
169+
}
170+
}
171+
```
172+
## JVM 종료
173+
JVM이 종료되는 두 가지 경우는 아래와 같다.
174+
1. 예정된 절차대로 종료
175+
2. 예기치 못하게 임의로 종료
176+
177+
절차에 맞춰 종료되는 경우 `일반` 스레드가 모두 종료되는 시점 또는 어디선가 `System.exit` 메소드를 호출하거나 기타 여러 가지 상황에 종료절차가 수행된다.
178+
### 종료 훅
179+
예정된 절차대로 종료되는 경우 JVM은 가장 먼저 등록되어 있는 모든 종료 훅`shutdown hook`을 수행한다.
180+
하나의 JVM에 여러 개의 종료훅을 등록할 수도 있으며, 두 개 이상의 종료 훅이 등록된 경우 어떤 순서로 훅을 실행하는지에 대해서는 규칙이 없다.
181+
182+
종료 훅이 모두 작업을 마치고 나면 JVM은 `runFinalizersOnExit` 값을 확인해 true라고 설정되어 있을경우 클래스의 finalize 메소드를 모두 호출하고 종료된다.
183+
JVM은 종료 과정에서 실행되고 있는 애플리케이션 내부의 스레드에 대해 중단 절차를 진행하거나 인터럽트를 걸지 않는다.
184+
185+
따라서 종료 훅은 스레드 안전하게 만들어야만 한다.이에 더해 애플리케이션의 상태에 대해 어떤 가정도 해서는 안되며, 아무런 가정 없이 올바로 동작할 수 있도록 굉장히 방어적인 형태로 만들어야 한다.
186+
187+
종료훅이 여러 개 등록되어 있는 경우 여러 훅이 동시에 실행되기 떄문에 다른 종료 훅에서 특정 서비스를 사용하고 있다면 문제가 될 수 있다.(사용중 중단되는 상황)
188+
이런 상황을 해결하기 위해 서비스별로 훅을 만들기보단 모든 서비스를 정리할 수 있는 하나의 종료훅을 사용해 각 서비스를 의존성에 맞춰 순서대로 정리하는 것도 방법이다.
189+
중요한 것은 어떤 방법을 사용하건 종료시 마무리 절차를 여러 개의 스레드를 사용해 동시에 처리하는 것보다는 순차적인 방법으로 차례대로 처리하면 문제점이 발생하는 경우를 줄일 수 있다.
190+
191+
### 데몬스레드
192+
데몬 스레드는 예를 들어 GC 등의 JVM 내부적으로 사용하기 위한 부수적인 스레드를 말한다. 이는 일반 스레드와 종료될 때 처리방법이 약간 다를 뿐 그외에는 모든 것이 동일하다.
193+
스레드 하나가 종료되면 JVM은 남은 스레드가 모두 데몬 스레드라면 즉시 JVM 종료 절차를 수행한다.(버려짐.) 이런 특성으로 인해 데몬 스레드는 보통 부수적인 용도로 사용하는 경우가 많다.
194+
195+
### finalize 메소드
196+
finalize() 메서드는 java.lang.Object 클래스에 정의되어 있으며, 자바에서 객체가 가비지 컬렉션에 의해 제거될 때 실행된다.
197+
즉, Finalizer는 자바에서 객체가 소멸될 때 마지막으로 수행할 수 있는 작업을 정의하는 데 사용된다. 주로 파일 핸들, 네트워크 연결, 데이터베이스 연결처럼 시스템 리소스를 정리하는 용도가 이런 작업이다.
198+
하지만, Finalizer는 예측할 수 없고, 상황에 따라 위험할 수 있어 불필요하며, 오작동, 낮은 성능, 이식성 문제의 원인으로 기본적으로는 쓰지 말아야 한다.
199+
200+
이는 동작방식상 GC의 대상이 되어야함 작동하기에 언제 실행될지 예측이 불가능하다. 또한 Finalizer스레드는 일반적으로 다른 스레드보다 낮은 우선순위가 있어 실행기회를 얻지 못할수도 있다.
201+
[대안책]
202+
- try-with-resources
203+
- tyr-finally 등등

0 commit comments

Comments
 (0)