자바의 스레드
🔍 JVM와 스레드
자바에서 생성되는 스레드는 사용자 수준 스레드이다. 즉, JVM에서 운영체제 레벨에 직접 접근할 수 없다. 따라서 JVM은 사용자 수준 스레드를 생성하고 JNI(Java Native Interface)를 통해 OS의 커널 스레드 생성하고 맵핑한다.
즉, 자바 스레드는 OS 스케줄러에 의해 실행 순서가 결정되며 스레드 실행 시점을 JVM에서 제어할 수는 없다.
🔍 스레드의 상태 그래프
- 쓰레드를 생성하고 실행하지 않은 상태를 NEW라고 하며, start() 메소드를 호출하면 실행 대기(Runnable) 상태가 된다.
- 실행 대기 상태에서 차례대로 실행이 된다.
- 주어진 실행시간이 다 되거나 yield()가 호출되면 다시 실행 대기상태가 된다.
- 실행 중에 suspend(), sleep(), wait(), join(), I/O block에 의해 일시정지상태가 될 수 있다.
- 일시정지시간이 다되거나 notify(), resume(), interrupt() 메서드가 호출되면 다시 실행대기 상태가 된다.
- 실행을 모두 마치면 소멸(TERMINATED) 된다.
🔍 스레드의 구현과 실행
스레드를 구현하는 방법에는 Thread.class
를 상속하는 방법과 Runnable
인터페이스를 구현하는 방법이 있다. 두 가지 방법 모두 run() 메서드를 오버라이딩하여 사용할 수 있다.
✏️ Thread 클래스
Thread 클래스는 Thread를 상속한 자식 인스턴스를 생성한 뒤 start() 메소드를 이용해 실행시킬 수 있다.
1
2
3
4
5
6
7
8
9
10
class ThreadEx extends Thread {
public void run() { ... }
}
class ThreadMain {
public static void main(String[] args) {
ThreadEx t = new ThreadEx();
t.start();
}
}
✏️ Runnable 인터페이스
Runnable을 상속한 자식 클래스를 생성한 뒤에 Thread(Runnable target) 생성자로 자식 클래스를 넘겨주면 된다.
1
2
3
4
5
6
7
8
9
10
11
public class ThreadEx2 implement Runnable {
public void run() { ... }
}
public class ThreadMain {
public static void main(String[] args) {
Runnable r = new ThreadEx2();
Thread t = new Thread(r);
t.start();
}
}
✏️ Runnable을 사용하자
Thread 클래스가 java.lang.Thread 클래스만 다루기 때문에 더 쉬워보이지만 Runnable이 더 많이 사용되고 있다. 이유는 다음과 같다.
- 자바는 다중 상속을 지원하지 않는다. 따라서 Thread를 사용한다는 것은 다른 클래스를 상속할 수 없음을 의미한다.
- Runnable 인터페이스는 Thread 또는 Executors 등에 의해 실행될 수 있기 때문에 Runnable을 사용하는 것이 좋은 디자인 결정이다.
- Runnable로 작업을 분리한다는 것은 그 작업을 재사용할 수 있으며 다른 실행자에 의해 실행될 수 있음을 의미한다.
Start()와 Run() 메서드
스레드를 실행하기 위해 Thread를 상속하고 run()
메서드를 오버라이딩 해서 run()을 호출해야 할 것 같지만 그렇지 않다. 스레드를 실행하기 위해서는 start()
메서드를 호출한다.
start() 메서드를 호출하면 스레드를 생성하면서 새로운 스택 영역을 확보하고 run() 메서드를 실행한다.
🔍 Java Thread API
✏️ sleep()
- 지정된 시간동안 스레드를 멈추게 한다.
1 2 3 4 5
void delay(long millis) { try { Thread.sleep(millis); } catch (InterruptedException e) {} }
✅ sleep(0)과 sleep(n)
sleep()은 네이티브 메서드로 메서드를 호출하면 시스템 콜을 호출하여 유저모드에서 커널모드로 전환된다. sleep(0)과 sleep(1)의 차이는 다음과 같다.
- sleep(0): 동일한 우선순위의 스레드가 있을 경우 실행대기 상태 스레드에게 CPU를 할당하고 컨텍스트 스위칭이 일어나며, 없다면 현재 스레드에서 모드 전환(사용자 수준 -> 커널 수준)만 일어난다.
- sleep(1): 현재 스레드를 대기 상태에 두고 다른 스레드에게 CPU를 할당한다.
✏️ interrupt()와 interrupted()
- 진행 중인 스레드 작업이 끝나기 전에 중지하는 경우에는
interrupt()
를 이용하여 작업을 중지시킬 수 있다. - interrupted()를 호출하면 interrupted상태를 반환 후
isInterrupted = false
로 업데이트한다.
1
2
3
4
5
6
7
8
// 쓰레드의 interrupted 상태를 false에서 true로 변경한다.
void interrupt()
// 쓰레드의 interrupted상태를 반환한다.
boolean isInterrupted
// 현재 쓰레드의 interrupted상태를 반환 후, false로 변경한다.
static boolean 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
public class InterruptExample {
public static void main(String[] args) {
Thread myThread = new MyThread();
myThread.start();
try {
Thread.sleep(2000); // 2초간 대기
} catch (InterruptedException e) {
e.printStackTrace();
}
// 스레드 인터럽트
myThread.interrupt();
}
}
class MyThread extends Thread {
public void run() {
while (!isInterrupted()) {
try {
System.out.println("스레드 실행 중");
} catch (InterruptedException e) {
System.out.println("인터럽트 발생");
// isInterrupted 상태는 false로 바뀐다.
// 스레드 인터럽트 상태를 체크하고 필요한 작업 수행
// 예: 자원 정리, 종료 등
return; // 스레드 종료
}
}
}
}
✏️ 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
class MyThread extends Thread {
private String name;
public MyThread(String name) {
this.name = name;
}
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(name + ": " + i);
// 스레드 실행 중간에 다른 스레드에게 실행 기회를 양보
Thread.yield();
}
}
}
public class Main {
public static void main(String[] args) {
MyThread thread1 = new MyThread("Thread 1");
MyThread thread2 = new MyThread("Thread 2");
thread1.start();
thread2.start();
}
}
✏️ Join()
- 다른 스레드가 종료될 때까지 실행을 중지하고 대기하다가 스레드가 종료되면 실행대기 상태로 전환된다.
- Object 클래스의 wait() 네이티브 메서드로 연결되며 시스템 콜을 통해 커널모드로 수행된다. 내부적으로 wait() & notify() 흐름을 가지고 제어한다.
스레드 그룹
자바는 스레드 그룹이라는 객체를 통해서 여러 스레드를 그룹화하는 편리한 방법을 제공한다.
- ThreadGroup은 스레드 집합을 나타내며 스레드 그룹에는 다른 스레드 그룹도 포함될 수 있다. 그룹 내의 모든 스레드는 한 번에 종료될 수 있다.
- 스레드는 반드시 하나의 스레드 그룹에 포함되어야 하며 명시적으로 표시하지 않으면 자신을 생성한 스레드가 속해 있는 스레드 그룹에 포함되어 진다.
- 일반적으로 사용자가 main스레드에서 생성하는 모든 스레드는 기본적으로 main스레드 그룹에 속하게 된다.
ThreadGroup mainThreadGroup = Thread.currentThread().getThreadGrooup();
ThreadGroup customThreadGroup = new ThreadGroup("Custom Thread Group");
🔗 참고
- 우아한형제들 - 자바 가상 스레드
- 자바의 정석
- 인프런 - 자바 동시성 프로그래밍(정수원)