Thread란
작업의 단위. 작업을 실행하는 주체
프로세스
프로세스는 프로그램을 실행 중인 프로그램을 뜻한다. 프로세스는 컴퓨터의 OS에 의해 CPU를 할당받아 동작한다.
스레드
이러한 프로세스를 구성하는 것이 스레드이다. 모든 프로세스 내에는 하나 이상의 스레드가 존재한다.
두 가지 이상의 스레드를 가지는 프로세스를 멀티스레드 프로세스(multi-threaded process)라고 한다.
Thread 구현
스레드 구현 방법에는 Runnable 인터페이스 구현, Thread 클래스 상속 두 가지가 있다.
두 방법 모두 run 메서드를 오버라이딩 하는 방식이다.
Runnable 인터페이스 구현
1
2
3
4
5
6
7
8
9
|
// Runnable 인터페이스 구현
class RunnableThread implements Runnable {
// run() 메서드 override
@Override
public void run() {
// 작업할 코드 작성
}
}
|
cs |
Thread 클래스 상속 구현
1
2
3
4
5
6
7
8
9
|
// Thread 클래스 상속
class ClassThread extends Thread {
// run() 메서드 override
@Override
public void run() {
// 작업할 코드 작성
}
}
|
cs |
Thread 생성
인터페이스 구현 방식(implements Runnable)
Runnable 인터페이스를 구현한 경우, 구현한 클래스를 인스턴스화 하고 Thread 생성자에 argument로 넘겨준다.
스레드 클래스의 메서드를 호출하기 위해서는 currentThread()를 호출하여 현재 스레드에 대한 참조를 얻어와야한다.
클래스 상속 방식(extends Thread)
Thread 클래스를 상속한 방식의 경우, 상속받은 클래스를 바로 thread 처럼 사용 가능하다.
스레드 클래스의 메서드를 바로 사용 가능하다.
🤓Thread 클래스를 상속받을 경우 다른 클래스를 상속받을 수 없기 때문에, 일반적으로 인터페이스 구현 방식을 사용한다.
코드
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
|
public class Main {
// Runnable 인터페이스 구현
static class RunnableThread implements Runnable {
// run() 메서드 override
@Override
public void run() {
// 인터페이스 구현 방식은 currentThread메서드로 현재 메서드의 상태를 불러와야함
for(int i = 0; i < 5; i++) {
System.out.println("runnable: " + Thread.currentThread().getName()); // 실행되는 스레드의 이름
try {
Thread.sleep(10); // 0.01초간 스레드 멈추기
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// Thread 클래스 상속
static class ClassThread extends Thread {
// run() 메서드 override
@Override
public void run() {
// 클래스 상속 방식은 스레드 관련 메서드 바로 사용 가능
for(int i = 0; i < 5; i++) {
System.out.println("class: " + getName()); // 실행되는 스레드의 이름
try {
Thread.sleep(10); // 0.01초간 스레드 멈추기
}
catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
// Runnable 구현 방식
Runnable runnable = new RunnableThread();
Thread thread = new Thread(runnable);
// 클래스 상속 방식
ClassThread classThread = new ClassThread();
}
}
|
cs |
Thread 실행
스레드에는 run()과 start() 메서드가 존재한다.
뜻만 본다면 스레드를 실행하기 위해서 run() 메서드를 호출해야 할 것 같지만, start() 메서드를 호출하는 것이 적절하다.
run()과 start()의 차이
run()과 start() 두 메서드 모두 스레드를 동작하게 하긴 한다. 하지만 스레드의 '실행' 목적에 부합하는 메서드는 start()이다.
- start() : 새로운 스레드 생성하고 생성된 스레드의 run() 메서드 수행
- run(): 새로운 스레드 생성하지 않고 기존에 사용하던 스레드에서의 run() 메서드 수행
스레드를 새로 생성하고 생성한 스레드를 실행한다는 점에서 멀티스레딩/스레드의 목적에 부합하는 메서드는 start()이다.
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
53
54
|
public class Main {
// Runnable 인터페이스 구현
static class RunnableThread implements Runnable {
// run() 메서드 override
@Override
public void run() {
// 인터페이스 구현 방식은 currentThread메서드로 현재 메서드의 상태를 불러와야함
for(int i = 0; i < 5; i++) {
System.out.println("runnable: " + Thread.currentThread().getName()); // 실행되는 스레드의 이름
try {
Thread.sleep(10);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// Thread 클래스 상속
static class ClassThread extends Thread {
// run() 메서드 override
@Override
public void run() {
// 클래스 상속 방식은 스레드 관련 메서드 바로 사용 가능
for(int i = 0; i < 5; i++) {
System.out.println("class: " + getName()); // 실행되는 스레드의 이름
try {
Thread.sleep(10); // 0.01초간 스레드 멈추기
}
catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
// Runnable 구현 방식
Runnable runnable = new RunnableThread();
Thread thread = new Thread(runnable);
// 클래스 상속 방식
// 바로 스레드로 사용이 가능하다.
ClassThread classThread = new ClassThread();
thread.run();
thread.start();
}
}
|
cs |
Thread 상태
스레드는 6가지 상태로 분류된다.
- New
- Runnable
- Blocked
- Waiting
- Timed Waiting
- Terminated
New 상태
스레드가 생성된 상태이다.
스레드가 생성된 직후인 new 상태에서는 스레드가 start()에 의해 실행되지 않은 상태이기에 스레드의 내부 코드 또한 동작하지 않는다.
Runnable 상태
스레드가 실행 가능한 상태로 전환된 상태이다.
해당 상태에서는 스레드가 동작되는 중이거나, 즉시 실행 가능한 상태이다.
스레드 스케줄러에 따라 해당 스레드의 동작 시점이 결정된다.
Blocked 상태
스레드가 동작하기 위해서는 lock을 점유해야 한다. 특정 스레드가 lock을 점유 중이라면 다른 스레드는 lock을 얻기 위해 기다리게 된다.
Blocked 상태는 스레드가 동작하기 위해서 필요한 lock을 얻기 위해서 대기하고 있는 상태이다.
lock을 얻게 된다면 스레드는 Blocked 상태에서 Runnable 상태로 전환된다.
Waiting 상태
wait() 이나 join() 메서드가 호출되면 스레드는 waiting 상태로 전환된다.
waiting 상태에서 다른 스레드가 notify() 메서드를 호출하거나, 스레드가 종료되면 runnable 상태로 전환된다.
Timed Waiting 상태
정해진 시간만큼 waiting 상태로 머물러 있는다.
waiting 상태 유지 시간은 메서드를 통해 받은 파라미터에 의해 결정된다.
Terminated 상태
스레드가 종료된 상태이다.
스레드 종료는 정상적으로 스레드 내부 동작을 모두 수행하고 정상적으로 종료되거나, 예견치 못한 에러로 인해 종료되는 두 가지의 경우가 있다.
코드
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
|
// 스레드 2 실행되면 아래 코드 실행됨
class Thread2 implements Runnable {
@Override
public void run() {
// 스레드2 1.5초동안 sleep
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("스레드1의 스레드2 join() 호출 이후 상태: " + Main.thread1.getState());
// 스레드2 0.2초동안 sleep
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Main implements Runnable{
public static Thread thread1;
public static Main obj;
public static void main(String[] args) {
obj = new Main();
thread1 = new Thread(obj); // 스레드1 생성
System.out.println("스레드1 생성 직후 상태: " + thread1.getState());
thread1.start(); // 스레드1 실행
System.out.println("스레드1 start 직후 상태: " + thread1.getState());
}
// 스레드1의 start() 메서드 호출되면 아래 코드 실행 됨
@Override
public void run() {
Thread2 myThread = new Thread2();
Thread thread2 = new Thread(myThread);
System.out.println("스레드2 생성 직후 상태: " + thread2.getState());
thread2.start(); // 스레드2 실행
System.out.println("스레드2의 실행 직후 상태: " + thread2.getState());
// 스레드1을 timed waiting 상태로 전환(0.2초)
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("스레드2의 sleep() 호출 직후 상태: " + thread2.getState());
try {
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("스레드2의 실행이 끝난 후 상태: " + thread2.getState());
}
}
|
cs |
Thread의 동기화
앞서 언급했던 스레드의 Blocked 상태와 밀접한 연관성이 있는 것이 바로 스레드의 동기화이다.
멀티스레딩 환경에서는 두 개 이상의 스레드가 프로세스 내의 공통된 자원에 접근하여 작업을 수행하고, 변경사항을 발생시킨다.
동기화 없이는 작업의 결과가 예상과는 다르게 나오는 오류가 발생할 수 있기 때문에 멀티스레딩에서의 스레드 동기화는 필수이다.
즉 하나의 프로세스 내의 스레드 간 자원 공유 및 작업을 가능하게 하는 것이 바로 스레드의 동기화이다.
스레드 동기화에는 mutex, semaphore, monitor 등의 기법이 있다.
이번에는 synchronized와 wait/notify 기법을 통한 스레드 동기화에 대해 알아보자.
Synchronized
멀티스레딩 환경에서 여러 스레드가 동시에 접근해서는 안되는 환경을 임계영역(Critical Section)이라고 한다.
임계영역을 적절하게 관리하지 않으면 데이터의 일관성을 해치기 때문에 임계영역에 대한 동기화를 수행한다.
아래는 Synchronized를 활용하여 임계영역을 통해 스레드 동기화를 적용시킨 것이다.
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 Main {
private static int studentCnt = 0; // 수강한 학생 인원 수
public static void main(String[] args) {
Thread t1 = new Thread(new ThreadObj());
Thread t2 = new Thread(new ThreadObj());
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("최종 수강인원: " + studentCnt);
}
static class ThreadObj implements Runnable {
@Override
public void run() {
// 수강신청
for(int i = 0; i < 20; i++) {
increaseStudent();
}
}
// 한 번에 하나의 스레드만 해당 메서드 수행 가능
private synchronized void increaseStudent() {
studentCnt++;
}
}
}
|
cs |
수강신청을 예시로 들어보자. 두 개의 스레드가 존재하고 각 스레드가 수강신청 인원 증가라는 작업을 처리한다.
만약 두 개의 스레드가 수강신청 인원이라는 자원에 동시에 접근해서 인원 증가 처리를 하게 된다면 어떻게 될까?
앞서 말한 데이터의 일관성을 해치는 오류가 발생할 것이다. 이를 막기 위해서 increaseStudent() 메서드를 임계영역으로 설정하였다.
wait/notify
전의 Object 클래스 포스트에서 설명했던 것과 같이, wait() 메서드가 호출되면 해당 스레드는 waiting 상태로 전환되고
waiting 상태로 전환된 스레드는 notify() / notifyAll() 메서드로 인해 RUNNABLE 상태로 전환된다.
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
|
import java.util.*;
public class Main {
public static void main(String[] args) throws InterruptedException {
Table table = new Table();
new Thread(new Cook(table), "cook1").start();
new Thread(new Customer(table, "burger"), "customer1").start();
new Thread(new Customer(table, "sundae"), "customer2").start();
Thread.sleep(2000);
System.exit(0);
}
static class Table {
String[] dishNames = {"burger", "burger", "sundae"};
final int MAX_FOOD = 6;
private ArrayList<String> dishes = new ArrayList<>();
public synchronized void add(String dish) { // 요리사가 음식 추가
while (dishes.size() >= MAX_FOOD) { // 음식이 꽉 찼을 경우
String name = Thread.currentThread().getName();
System.out.println(name + " is waiting.");
try {
wait(); // Cook 스레드 sleep
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
dishes.add(dish);
notify(); // 기다리는 Customer 스레드 깨우기
System.out.println("Dishes on the table: " + dishes);
}
public void remove(String dish) { // 손님이 음식 소비
synchronized (this) {
String name = Thread.currentThread().getName();
while(dishes.isEmpty()) { // 현재 음식이 없으면 손님을 기다리게 함
System.out.println(name + " is waiting.");
try {
wait(); // Customer 스레드 대기
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
while (true) {
for (int i = 0; i < dishes.size(); i++) {
if (dishes.get(i).equals(dish)) {
dishes.remove(i);
notify(); // Cook 스레드 notify
return;
}
}
try {
System.out.println(name + " is waiting.");
wait(); // 원하는 음식이 없는 Customer 스레드 대기 처리
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public int dishNum() {
return dishNames.length;
}
}
static class Customer implements Runnable {
private Table table;
private String food;
Customer(Table table, String food) {
this.table = table;
this.food = food;
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
String name = Thread.currentThread().getName();
table.remove(food);
System.out.println(name + " ate " + food);
}
}
}
static class Cook implements Runnable {
private Table table;
Cook(Table table) {
this.table = table;
}
@Override
public void run() {
while (true) { // 요리 랜덤하게 만들고 테이블에 추가하기
int idx = (int)(Math.random() * table.dishNum());
table.add(table.dishNames[idx]);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
|
cs |
References
https://gyoogle.dev/blog/computer-language/Java/Thread.html
https://tcpschool.com/java/java_thread_concept
https://www.geeksforgeeks.org/difference-between-thread-start-and-thread-run-in-java/
https://www.geeksforgeeks.org/lifecycle-and-states-of-a-thread-in-java/
'Java' 카테고리의 다른 글
[Java] 고유 락(Intrinsic Lock) (0) | 2024.04.08 |
---|---|
[Java] Object 클래스 (0) | 2024.04.07 |
[Java] 가비지 컬렉션(Garbage Collection) (0) | 2024.04.06 |
[Java] Stream(스트림) (1) | 2024.04.06 |
[Java] Interned String (0) | 2024.04.06 |