스레드 제어와 생명 주기
스레드 기본 정보
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class ThreadInfoMain {
public static void main(String[] args) {
// main 스레드
Thread mainThread = Thread.currentThread();
log("main mainThread = " + mainThread);
// 고유 식별자
log("mainThread.threadId() = " + mainThread.threadId());
// 스레드 이름
log("mainThread.getName() = " + mainThread.getName());
// 우선 순위 1 ~ 10까지 있음
log("mainThread.getPriority() = " + mainThread.getPriority());
// 스레드가 속한 그룹
// 기본적으로 모든 스레드는 부모 스레드와 동일한 그룹에 속함
log("mainThread.getThreadGroup() = " + mainThread.getThreadGroup());
// 스레드 상태
log("mainThread.getState() = " + mainThread.getState());
}
}
스레드 생명 주기
스레드 상태
New: 스레드가 생성되었으나 아직 시작되지 않은 상태Runnable: 스레드가 실행 중이거나 실행될 준비가 된 상태Blocked: 스레드가 동기화 락을 기다리는 상태Waiting: 스레드가 무기한으로 다른 스레드의 작업을 기다리는 상태Timed Waiting: 스레드가 일정 시간 동안 다른 스레드의 작업을 기다리는 상태Terminated: 스레드의 실행이 완료된 상태
스레드의 작업이 끝나기를 기다리기 - Join
예시
1 ~ 100까지 더하는 작업을 스레드1이 1~50까지 더하고 스레드2가 51~100을 더해서 결과로 나온 값을 더하는 로직이 필요하다고 가정해보겠습니다.
join() 사용 안하고 해결
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class JoinMainV2 {
public static void main(String[] args) {
log("start");
SumTask sumTask1 = new SumTask(1, 50);
SumTask sumTask2 = new SumTask(51, 100);
Thread thread1 = new Thread(sumTask1, "thread-1");
Thread thread2 = new Thread(sumTask2, "thread-2");
thread1.start();
thread2.start();
// 정확한 타이밍을 맞추어 기다리기 어려움
log("main thread sleep");
sleep(3000);
log("main thread 깨어남");
log("task1.result = " + sumTask1.result);
log("task2.result = " + sumTask2.result);
log("end");
}
static class SumTask implements Runnable {
int startValue;
int endValue;
int result = 0;
public SumTask(int startValue, int endValue) {
this.startValue = startValue;
this.endValue = endValue;
}
@Override
public void run() {
log("작업 시작");
int sum = 0;
for (int i = startValue; i <= endValue; i++) {
sum += i;
}
sleep(2000);
result = sum;
log("작업 완료 result = " + result);
}
}
}
각 작업이 2초 정도의 시간이 걸린다고 가정하겠습니다. 이런 경우 두 작업이 다 끝나고 나서 결과 값을 조회해야 하므로 main 스레드에서 3초 정도 대기 후 조회 하도록 해야 합니다.
실행 시간이 2초라고 가정했지만, 실제로는 각 작업이 얼마나 걸릴지 모르는 병렬 처리가 필요한 순간이 더 많습니다.
이럴 때 join() 메서드를 사용하여 처리하면 효율적으로 처리 할 수 있습니다.
join() 사용 하여 해결
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class JoinMainV3 {
public static void main(String[] args) throws InterruptedException {
log("start");
SumTask sumTask1 = new SumTask(1, 50);
SumTask sumTask2 = new SumTask(51, 100);
Thread thread1 = new Thread(sumTask1, "thread-1");
Thread thread2 = new Thread(sumTask2, "thread-2");
thread1.start();
thread2.start();
// 스레드가 종료 될 때 까지 대기
log("join() main 스레드가 thread1, thread2 종료까지 대기");
thread1.join();
thread2.join();
log("main 스레드 대기 완료");
log("task1.result = " + sumTask1.result);
log("task2.result = " + sumTask2.result);
log("end");
}
static class SumTask implements Runnable {
int startValue;
int endValue;
int result = 0;
public SumTask(int startValue, int endValue) {
this.startValue = startValue;
this.endValue = endValue;
}
@Override
public void run() {
log("작업 시작");
int sum = 0;
for (int i = startValue; i <= endValue; i++) {
sum += i;
}
sleep(2000);
result = sum;
log("작업 완료 result = " + result);
}
}
}
start()호출 시 스레드 시작thread1,thread2둘 다start()호출 했으므로 병렬로 실행 됨
- join()
호출 시,main 스레드는 해당 스레드가 끝나기를 대기 (Wating` 상태)thread1.join()이 끝나면thread2.join()실행
- 작업이 끝나면
main 스레드다시 실행
잘못된 사용법
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class JoinTest1Main {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new MyTask(), "t1");
Thread t2 = new Thread(new MyTask(), "t2");
Thread t3 = new Thread(new MyTask(), "t3");
t1.start();
t1.join();
t2.start();
t2.join();
t3.start();
t3.join();
System.out.println("모든 스레드 실행 완료");
}
static class MyTask implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 3; i++) {
log(i);
sleep(1000);
}
}
}
}
위 처럼 사용 시, 스레드 시작하고 작업이 끝나면 다음 스레드를 시작하고 하므로 총 9초의 시간이 걸립니다.
이는 스레드를 병렬로 사용하는 방법이 아닙니다.
올바른 사용법
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class JoinTest2Main {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new MyTask(), "t1");
Thread t2 = new Thread(new MyTask(), "t2");
Thread t3 = new Thread(new MyTask(), "t3");
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println("모든 스레드 실행 완료");
}
static class MyTask implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 3; i++) {
log(i);
sleep(1000);
}
}
}
}
위 처럼 실행하면 3개의 스레드가 동시에 실행되고, 스레드 별로 끝나기를 기다리므로 3초만에 끝나게 됩니다.
특정 시간만큼만 기다리기
join() 메서드는 기본적으로 해당 작업이 끝날 때 까지 무한정 기다립니다. join() 메서드에 인자로 ms를 넘겨주면 해당 시간만큼만 기다리고 다시 main 스레드를 돌아와 실행하도록 할 수도 있습니다.
1
thread1.join(5000) // 5초만 기다리기 (Timed Waitng 상태)
스레드 작업을 중간에 중단하기 - Interrupt
예시
변수를 이용하여 중단하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class ThreadStopMainV1 {
public static void main(String[] args) {
MyTask myTask = new MyTask();
Thread thread = new Thread(myTask, "work");
thread.start();
sleep(4000);
log("작업중단 지시 runFlag = false");
myTask.runFlag = false;
}
static class MyTask implements Runnable {
volatile boolean runFlag = true;
@Override
public void run() {
while (runFlag) {
log("작업 중");
sleep(3000);
}
log("자원 정리");
log("작업 종료");
}
}
}
위 방법은 runFlag의 값을 변경하여 작업을 종료하는 방법입니다. 하지만 이 방법은 바로 종료 되지 않고 sleep()(대기 상태)이 끝난 다음에 종료되게 됩니다.
interrupt()를 이용하여 중단하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class ThreadStopMainV2 {
public static void main(String[] args) {
MyTask myTask = new MyTask();
Thread thread = new Thread(myTask, "work");
thread.start();
sleep(4000);
log("작업중단 지시 thread.interrupt()");
thread.interrupt(); // interrupt로 해당 스레드 중단
log("work스레드 인터럽트 상태1 = " + thread.isInterrupted());
}
static class MyTask implements Runnable {
@Override
public void run() {
try {
while (true) {
log("작업 중");
Thread.sleep(3000);
}
} catch (InterruptedException e) {
log("work 스레드 인터럽트 상태2 = " + Thread.currentThread().isInterrupted());
log("interrupt message = " + e.getMessage());
log("state = " + Thread.currentThread().getState());
}
log("자원 정리");
log("작업 종료");
}
}
}
위 처럼 코드를 변경하면 InterruptedException 예외가 발생하여 해당 스레드가 종료 됩니다.
thread.interrupt()를 실행했다고 해서 바로InterruptedException예외가 발생하는 것은 아닙니다.
InterruptedException예외를 발생시키는 로직을 만났을 때 (여기선sleep()) 예외가 발생하게 됩니다.
interrupt 상태 확인하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class ThreadStopMainV3 {
public static void main(String[] args) {
MyTask myTask = new MyTask();
Thread thread = new Thread(myTask, "work");
thread.start();
sleep(100); // 시간을 줄임
log("작업중단 지시 thread.interrupt()");
thread.interrupt();
log("work스레드 인터럽트 상태1 = " + thread.isInterrupted());
}
static class MyTask implements Runnable {
@Override
public void run() {
// 코드 수정
while (!Thread.currentThread().isInterrupted()) { // 인터럽트 상태 변경 X
log("작업 중");
}
log("work 스레드 인터럽트 상태2 = " + Thread.currentThread().isInterrupted());
try {
log("자원 정리");
Thread.sleep(1000);
log("자원 종료");
} catch (InterruptedException e) {
log("자원 정리 실패 - 자원 정리 중 인터럽트 발생");
log("work 스레드 인터럽트 상태3 = " + Thread.currentThread().isInterrupted());
}
log("작업 종료");
}
}
}
위 로직은 인터럽트 상태를 확인하여 로직을 실행할 지 판단하도록 수정하였습니다.
하지만 인터럽트 상태가 true이면 InterruptedException 예외를 발생 시키는 로직을 만나야 InterruptedException 예외를 발생시키고 인터럽트 상태를 false로 변경하는데 위 while문에서는 상태를 “확인만” 하므로 상태 값이 여전히 true입니다.
따라서 sleep()을 만나야 InterruptedException 예외가 발생하게 되고, 자원 종료를 정상적으로 하지 못하게 됩니다.
Thread.interrupted를 이용하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class ThreadStopMainV4 {
public static void main(String[] args) {
MyTask myTask = new MyTask();
Thread thread = new Thread(myTask, "work");
thread.start();
sleep(100); // 시간을줄임
log("작업중단 지시 thread.interrupt()");
thread.interrupt();
log("work스레드 인터럽트 상태1 = " + thread.isInterrupted());
}
static class MyTask implements Runnable {
@Override
public void run() {
// 코드 수정
while (!Thread.interrupted()) { // 인터럽트 상태 변경 O
log("작업 중");
}
log("work 스레드 인터럽트 상태2 = " + Thread.currentThread().isInterrupted());
try {
log("자원 정리");
Thread.sleep(1000);
log("자원 종료");
} catch (InterruptedException e) {
log("자원 정리 실패 - 자원 정리 중 인터럽트 발생");
log("work 스레드 인터럽트 상태3 = " + Thread.currentThread().isInterrupted());
}
log("작업 종료");
}
}
}
Thread.interrupted() 메서드는 상태를 확인하면서 상태 값을 false 로 변경합니다.
따라서 sleep() 로직을 만나도 InterruptedException 예외가 발생하지 않고 정상적으로 자원을 종료합니다.
yield
어떤 스레드를 얼마나 실행 할지는 스케줄링을 통해 결정합니다. yield를 사용하면 다른 스레드에게 실행 기회를 양보할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class YieldMain {
static final int THREAD_COUNT = 1000;
public static void main(String[] args) {
for (int i = 0; i < THREAD_COUNT; i++) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}
}
static class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
// 1. empty
System.out.println(Thread.currentThread().getName() + " - " + i);
// 2. sleep(1)
// sleep(1);
// 3. yield
Thread.yield();
}
}
}
}
sleep()사용TIMED_WAITING상태가 되어 다른 스레드에게 실행 기회 양보- 하지만
RUNNABLE→TIMED_WAITING→RUNNABLE로 복잡한 과정을 거치게 됨
yield사용- 다른 스레드에게 실행 기회를 넘기고 다시 스케줄러로 들어감 (
RUNNABLE→RUNNABLE) - 따라서
sleep()보다 효율적
- 다른 스레드에게 실행 기회를 넘기고 다시 스케줄러로 들어감 (
