Java. 멀티 스레드와 동시성
프로세스와 스레드
CPU가 여러개의 프로그램을 대략 0.01초 정도 수행하다가 잠시 멈추고 번갈아하면서 수행한다.
멀티태스킹이란, 하나의 컴퓨터 시스템이 동시에 여러 작업을 수행하는 능력이다.
운영체제가 스케줄링을 수행하고, CPU를 최대한 사용하면서 작업이 골고루 수행될 수 있게 최적화된다 라고만 이해하면 된다.
- 시분할: 마치 동시에 실행 되는 것처럼 하는 기법
- 스케줄링: 어떤 프로그램이 얼마만큼 실행될지는 운영체제가 결정하는 것
멀티프로세싱이란, 컴퓨터 시스템에서 둘 이상의 프로세서(CPU 코어)를 사용하여 여러 작업을 동시에 처리하는 기술을 의미한다.
멀티프로세싱: 하드웨어 기반
멀티태스킹 : 소프트웨어 기반
여러 CPU 코어를 사용 = 멀티프로세싱
단일 CPU 코어 관점에서 여러 작업을 분할해서 수행 = 멀티태스킹
프로세스란, 운영체제 안에서 실행중인 프로그램을 의미한다.
각 프로세스는 별도의 메모리 공간을 갖고 있기 때문에 서로 간섭하지 않는다.
프로세스가 서로의 메모리에 직접 접근할 수 없다.
심각한 문제가 발생해도, 다른 프로세스에 영향을 주지 않는다.
프로세스의 메모리
- 코드 섹션: 실행할 프로그램의 코드가 저장
- 데이터 섹션: 전역 변수 및 정적 변수가 저장
- 힙: 동적으로 할당되는 메모리 영역
- 스택: 메서드 호출 시 생성되는 지역 변수와 반환 주소가 저장되는 영역 (스레드에 포함)
스레드는 프로세스는 하나 이상의 스레드를 포함한다.
한 프로세스의 스레드들은 서로 메모리 공간을 공유한다.
자신의 프로세스의 메모리끼리 공유할 수 있고 각 스레드는 자신의 스택을 가지고 있다.
프로그램이 실행된다는 것은 사실 프로세스 안에 코드가 한줄 씩 실행되는 것이다.
이렇듯 프로세스의 코드를 실행하는 흐름을 스레드라 한다.
스레드는 프로세스 내에서 실행되는 작업의 단위이다. 즉, 코드 하나하나 실행하는 것을 스레드라고 한다.
멀티 스레드가 필요한 이유
하나의 프로그램도 그 안에서 동시에 여러 작업이 필요하다.
단일 코어 스케줄링
운영체제는 내부에 스케줄링 큐를 가지고 있다. 스레드는 스케줄링 큐에 대기한다.
모든 계산은 CPU에서 수행되기 때문에, 큐에서 꺼내서 CPU에서 실행된다. 실제로 CPU에 의해 실행되는 단위는 스레드이다.
멀티 코어 스케줄링
다시 한번 말하자면 스케줄링이란, CPU에 어떤 프로그램이 얼마만큼 실행될지 운영체제가 결정짓는 것
스케줄링의 우선 순위, 최적화 기법 등은 내가 배운 운영체제론을 다시 되짚어보자.
컨텍스트 스위칭
스레드A를 수행하다가 스레드 B를 수행한다.
그 뒤 스레드A로 다시 돌아갈 때 어디에서부터 작업을 했는지 기억을 해야한다.
컨텍스트 스위칭 과정에서 이전에 실행 중인 값을 메모리 잠깐 저장하고, 이후에 다시 실행하는 시점에 저장한 값을 CPU에 다시 불러와야한다.
멀티스레드는 대부분 효율적이지만, 컨텍스트 스위칭 과정이 필요하므로 항상 효율적인 것은 아니다.
CPU 코어수 + 1개 정도로 스레드를 맞추면 특정 스레드가 잠시 대기할 때 대기할 때 남은 스레드를 활용할 수 있다.
하지만, 위의 말이 맞는 말은 아니다.
CPU 바운드 작업 vs I/O 바운드 작업
CPU 바운드 작업: 연산 능력을 많이 요구하는 작업
I/O 바운드 작업: I/O 작업이 완료될 때까지 대기 시간이 많이 발생, CPU는 상대적으로 유휴 상태에 있는 경우가 많다.
웹 어플리케이션 서버:
실무에서는 I/O 바운드 작업이 많다.
백엔드는 CPU 연산이 필요한 작업 보다는, I/O바운드 작업이 많다.
스레드 숫자는 CPU-바운드 작업이 많은가, 아니면 I/O-바운드 작업이 많은가에 따라 다르게 설정해야한다.
사용자 스레드와 데몬 스레드
데몬 스레드란, 백그라운드에서 보조적인 작업을 수행하며, 모든 사용자 스레드가 종료되면 데몬 스레드도 자동으로 종료된다.
보조적인 작업이 필요하면, 데몬 스레드를 만들면 된다
public class DaemonThreadMain {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + ": main() start");
DaemondThread daemondThread = new DaemondThread();
daemondThread.setDaemon(true); // 데몬 스레드 여부
daemondThread.start();
System.out.println(Thread.currentThread().getName() + ": main() end");
}
static class DaemondThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ": run()");
try {
Thread.sleep(10000); //10초간 실행 후 종료
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + ": end()");
}
}
}
main: main() start
main: main() end
Thread-0: run()
Process finished with exit code 0
코드를 보면 run()
메소드에서 10초 후 end()
를 출력하도록 되어있는데,
실행해보면 사용자 스레드인 main()
스레드가 종료되자마자, DaemonThread
의 데몬스레드 run()
도 동시에 종료가 된다.setDaemon()
설정은 start()
를 호출하기전에 결정해야한다. 기본값은 false
이다 (사용자 스레드).
스레드를 만들 때는 Thread
클래스를 상속 받는 방법과 Runnable
인터페이스를 구현하는 방법이 있다.
참고로 Runnable
인터페이스 구현방식을 실무에서 써야한다.
public class HelloRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ": run()");
}
}
package thread.start;
public class HelloRunnableMain {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + ": main() start");
HelloRunnable runnable = new HelloRunnable();
Thread thread = new Thread(runnable);
thread.start();
System.out.println(Thread.currentThread().getName() + ": main() end");
}
}
Thread
상속:
- 상속의 제한
- 유연성 부족
Runnable
인터페이스 구현:
- 상속의 자유로움
- 코드의 분리: 스레드와 실행할 작업을 분리할 수 있다.
- 여러 스레드가 동일한
Runnable
객체를 공유할 수 있어 자원 관리를 효율적으로 할 수 있다.
###Runnable을 만드는 다양한 방법
- 정적 중첩 클래스로 구현
public class InnerRunnableMainV1 { public static void main(String[] args) { log("main() start"); MyRunnable runnable = new MyRunnable(); Thread thread = new Thread(runnable); thread.start(); log("main() end"); } static class MyRunnable implements Runnable { @Override public void run() { log("run()"); } } }
2. 익명 클래스로 구현
```java
public class InnerRunnableMainV2 {
public static void main(String[] args) {
log("main() start");
Runnable runnable = new Runnable() {
@Override
public void run() {
log("run()");
}
};
Thread thread = new Thread(runnable);
thread.start();
log("main() end");
}
}
- 변수없이 익명 클래스로 구현
public class InnerRunnableMainV2 { public static void main(String[] args) { log("main() start");
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
log("run()");
}
});
thread.start();
log("main() end");
}
}
4. 람다
```java
public class InnerRunnableMainV2 {
public static void main(String[] args) {
log("main() start");
Thread thread = new Thread(() -> log("run()"));
thread.start();
log("main() end");
}
}
스레드 기본 정보
package thread.control;
import thread.start.HelloRunnable;
import util.MyLogger;
import static util.MyLogger.*;
public class ThreadInfoMain {
public static void main(String[] args) {
//main 스레드
Thread mainThread = Thread.currentThread();
log("mainThread = " + mainThread);
log("mainThread.threadId()= " + mainThread.threadId());
log("mainThread.getName()= " + mainThread.getName());
log("mainThread.getPriority()= " + mainThread.getPriority()); //default: 5
log("mainThread.getThreadGroup()= " + mainThread.getThreadGroup());
log("mainThread.getState()= " + mainThread.getState());
//myThread 스레드
Thread myThread = new Thread(new HelloRunnable(), "myThread");
log("myThread = " + myThread);
log("myThread.threadId()= " + myThread.threadId());
log("myThread.getName()= " + myThread.getName());
log("myThread.getPriority()= " + myThread.getPriority()); //default: 5
log("myThread.getThreadGroup()= " + myThread.getThreadGroup());
log("myThread.getState()= " + myThread.getState());
}
}
실행 결과
16:25:01.786 [ main] mainThread = Thread[#1,main,5,main]
16:25:01.791 [ main] mainThread.threadId()= 1
16:25:01.791 [ main] mainThread.getName()= main
16:25:01.796 [ main] mainThread.getPriority()= 5
16:25:01.796 [ main] mainThread.getThreadGroup()= java.lang.ThreadGroup[name=main,maxpri=10]
16:25:01.797 [ main] mainThread.getState()= RUNNABLE
16:25:01.797 [ main] myThread = Thread[#21,myThread,5,main]
16:25:01.797 [ main] myThread.threadId()= 21
16:25:01.797 [ main] myThread.getName()= myThread
16:25:01.798 [ main] myThread.getPriority()= 5
16:25:01.798 [ main] myThread.getThreadGroup()= java.lang.ThreadGroup[name=main,maxpri=10]
16:25:01.798 [ main] myThread.getState()= NEW
Thread
클래스의 toString()
: 스레드 ID, 스레드 이름, 우선 순위, 스레드 그룹threadId()
: JVM 내에 각 스레드에 대한 유일한 ID, 직접 지정할 수 없다.getName()
: 스레드 이름은 중복이 될 수 있다.getPriority()
: 우선순위 1(가장 낮음) ~ 10 (가장 높음) default: 5, 단 실행 순서는 JVM구현과 운영체제에 따라 달라질 수 있다.getThreadGroup()
: 스레드가 속한 스레드 그룹을 반환하는 메소드. 스레드 그룹은 여러 스레드를 하나의 그룹으로 묶어서 작업(일괄 종료, 우선순위 설정 등) 을 수행할 수 있다.
부모 스레드: 새로운 스레드를 생성해준 스레드를 부모 스레드라고 한다.getState()
: 스레드 상태
스레드 생명 주기
- NEW : 스레드가 아직 시작되지 않은 상태
예:Thread thread = new Thread(runnable);
start()
메서드가 호출되지 않은 상태 - RUNNABLE : 스레드가 실행 중이거나 실행될 준비가 된 상태
start()
메서드가 호출되면 스레드는 이 상태로 돌아간다.
운영체제의 스케줄러 실행 대기열에 포함되어 있다가 차례로 CPU에서 실행된다.
스케줄러에서 실행 대기열에 있든, CPU에서 실제 실행되고 있는 모두RUNNABLE
상태이다. - BLOCKED : 스레드가 동기화 락을 기다리는 상태
스레드가 다른 스레드에 의해 동기화 락을 얻기 위해 기다리는 상태
예:synchronized
블록에 진입하기 위해 락을 얻어야하는 경우 이 상태에 들어간다. - WATING : 스레드가 다른 스레드의 특정 작업이 완료되기를 기다리는 상태
스레드가 다른 스레드의 특정 작업이 완료되기를 무기한 기다리는 상태이다.
예:wait()
,join()
메서드가 호출될 때 이 상태가 된다. - TIMED_WATING : 일정 시간 동안 기다리는 상태
스레드가 특정 시간 동안 다른 스레드의 작업이 완료되기를 기다리는 상태 - TERMINATED : 스레드가 실행을 마친 상태
스레드의 실행이 완료된 상태 (정상 종료 또는 예외 발생)
스레드는 한 번 종료되면 다시 시작할 수 없다.
public class ThreadStateMain {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new MyRunnable(), "myThread");
log("myThread.state1 = " + thread.getState()); //NEW
log("myThread.start()");
thread.start();
Thread.sleep(1000);
log("myThread.state3 = " + thread.getState()); //TIMED_WAITING
Thread.sleep(4000);
log("myThread.state5 = " + thread.getState()); //TERMINATED
}
static class MyRunnable implements Runnable {
@Override
public void run() {
try {
log("start");
log("myThread.state2 = " + Thread.currentThread().getState()); //RUNNABLE
log("sleep() start");
Thread.sleep(3000);
log("sleep() end");
log("myThread.state4 = " + Thread.currentThread().getState()); //RUNNABLE
log("end");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
17:31:06.582 [ main] myThread.state1 = NEW
17:31:06.584 [ main] myThread.start()
17:31:06.584 [ myThread] start
17:31:06.584 [ myThread] myThread.state2 = RUNNABLE
17:31:06.585 [ myThread] sleep() start
17:31:07.588 [ main] myThread.state3 = TIMED_WAITING
17:31:09.588 [ myThread] sleep() end
17:31:09.589 [ myThread] myThread.state4 = RUNNABLE
17:31:09.589 [ myThread] end
17:31:11.594 [ main] myThread.state5 = TERMINATED
Thread.currentThread()
를 호출하면 해당 코드를 실행하는 스레드 객체를 조회할 수 있다.
체크 예외 재정의Runnable
의 run()
메서드를 구현할 때 InterruptedException
체크 예외를 밖으로 던질 수 없는 이유
- 체크 예외
부모 메서드가 체크 예외를 던지지 않는 경우, 재정의 된 자식 메서드도 체크 예외를 던질 수 없다.
자식 메서드는 부모 메서드가 던질 수 있는 체크 예외의 하위 타입만 던질 수 있다.
Runnable
인터페이스의 run()
메서드는 아무런 체크 예외를 던지지 않기 때문에, 밖으로 던질 수 없다.
자바는 왜 이런 제약을 두는 것일까?
자식 메소드가 부모 메소드의 예외 처리보다 상위 타입의 예외 처리를 한다면,Parent p = new Child()
p.method()
를 하면, 컴파일러는 부모 메소드의 예외처리만 알 수 있기때문에, 자식 타입의 예외는 처리하지 못한다.
그래서 어긋나기 때문에, 부모 타입의 예외 처리보다 더 상위 타입의 예외처리는 할 수 없게 되는 것이다.
그렇다면 왜 인터페이스 Runnable
의 run()
을 만든 개발자분은 왜 체크 예외를 던지지 않았을까?
개발자는 반드시 체크 예외 try-catch 블록 내에 처리하게 된다.
이는 예외 발생 시 예외가 적절히 처리되지 않아서 프로그램이 비정상 종료되는 상황을 방지할 수 있다.
특히 멀티스레드 환경에서는 예외 처리를 강제함으로써 스레드의 안정성과 일관성을 유지할 수 있다.
하지만 체크 예외를 강제하는 이런 부분들은 자바 초창기 기조이고, 최근에는 체크 예외 보다는 언체크 예외를 선호한다.
스레드 제어와 생명 주기
각 스레드에는 스택이 존재한다.
스레드는 메서드의 호출을 관리하기 위해 메서드 단위로 스택 프레임을 만들고 해당 스택 프레임을 스택 위에 쌓아올린다.
스택 프레임에는 this
의 참조값을 저장하기 때문에, 특정 인스턴스의 메서드를 호출하면 this
의 값을 불러서 사용한다.
클래스의 메소드를 호출하면 당연히 this
는 없을 것이다.
public class JoinMainV3 {
public static void main(String[] args) throws InterruptedException {
log("start");
SumTask task1 = new SumTask(1, 50);
SumTask task2 = new SumTask(51, 100);
Thread thread1 = new Thread(task1, "thread-1");
Thread thread2 = new Thread(task2, "thread-2");
thread1.start();
thread2.start();
//스레드가 종료될 때 까지 대기
log("join() - main 스레드가 thread1, thread2 종료까지 대기");
thread1.join();
thread2.join();
log("main 스레드 대기 완료");
log("task1.result = " + task1.result);
log("task2.result = " + task2.result);
int sumAll = task1.result + task2.result;
log("task1 + task2 = " + sumAll);
log("end");
}
static class SumTask implements Runnable {
private int startValue;
private int endValue;
private int result;
public SumTask(int startValue, int endValue) {
this.startValue = startValue;
this.endValue = endValue;
}
@Override
public void run() {
log("작업 시작");
sleep(2000);
int sum = 0;
for (int i = startValue; i <= endValue; i++) {
sum += i;
}
result = sum;
log("작업 완료 result = " + result);
}
}
}
15:11:11.394 [ main] start
15:11:11.397 [ thread-1] 작업 시작
15:11:11.397 [ main] join() - main 스레드가 thread1, thread2 종료까지 대기
15:11:11.397 [ thread-2] 작업 시작
15:11:13.407 [ thread-2] 작업 완료 result = 3775
15:11:13.407 [ thread-1] 작업 완료 result = 1275
15:11:13.407 [ main] main 스레드 대기 완료
15:11:13.408 [ main] task1.result = 1275
15:11:13.408 [ main] task2.result = 3775
15:11:13.409 [ main] task1 + task2 = 5050
15:11:13.409 [ main] end
join()
메소드는 해당 스레드가 종료될 때까지 WAITING
상태였다가, 해당 스레드가 TERMINATED
되면 RUNNABLE
상태가 되고, 계속해서 스레드가 실행된다.
thread1.join();
thread2.join();
위의 코드는 thread1
과 thread2
가 동시에 join()
되는 것이 아니다.thread1
이 TERMINATED
되면 main
스레드가 RUNNABLE
상태가 되고, thread2
가 join()
를 실행 것이다.
물론, thread2
도 기다릴 때는 main
스레드는 WAITING
상태가 된다.
이렇듯 특정 스레드가 완료될 때까지 기다려야 하는 상황이라면 join()
을 사용하면 된다.
하지만, join()
의 단점은 다른 스레드가 완료될 때까지 무기한 기다려야한다.
특정 시간 만큼만 대기하려면 join(ms)
를 호출하면 특정 시간 만큼만 대기가 된다.
- 이때
main
스레드의 상태는WAITING
이 아니라,TIMED_WAITING
이 된다. - 보통
WAITING
은 무기한 대기, 특정 시간 만큼 대기는TIMED_WAITING
이다.
스레드 제어와 생명 주기2
public class ThreadStopMainV1 {
public static void main(String[] args) {
MyTask task = new MyTask();
Thread thread = new Thread(task, "work");
thread.start();
sleep(4000);
log("작업 중단 지시 runFlag=false");
task.runFlag = false;
}
static class MyTask implements Runnable {
volatile boolean runFlag = true;
@Override
public void run() {
while (runFlag) {
log("작업 중");
sleep(3000);
}
log("자원 정리");
log("자원 종료");
}
}
}
위의 코드는 work
스레드가 작업 하다가, 중간에 runFlag=false
가 되어 중단되어 종료되는 코드이다.
23:04:13.761 [ work] 작업 중
23:04:16.764 [ work] 작업 중
23:04:17.745 [ main] 작업 중단 지시 runFlag=false
23:04:19.766 [ work] 자원 정리
23:04:19.767 [ work] 자원 종료
문제점은 작업 중단 지시 2초 정도 이후에 자원을 정리하고 작업을 종료한다.
즉, 작업 중단을 지시해도 즉각 종료하지 않는다.
sleep()
처럼 스레드가 대기하는 상태에서도 어떻게 하면 즉각 깨울 수 있을까?
인터럽트를 사용하면, WAITING
, TIMED_WATING
같은 대기 상태의 스레드를 직접 깨워서 RUNNABLE
상태로 만들 수 있다.
package thread.control.interrupt;
import static util.MyLogger.log;
import static util.ThreadUtils.sleep;
public class ThreadStopMainV2 {
public static void main(String[] args) {
MyTask task = new MyTask();
Thread thread = new Thread(task, "work");
thread.start();
sleep(4000);
log("작업 중단 지시 thread.interrupt()");
thread.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("자원 종료");
}
}
}
interrupt()
메서드를 호출하면 해당 스레드에 인터럽트가 발생한다.
인터럽트가 발생하면 해당 스레드에 InterruptedException
이 발생한다.
- 이때 인터럽트가 깨어나고
RUNNABLE
상태가 되고, 코드를 정상 수행한다. - 이때
InterruptedException
을catch
로 잡아서 정상 흐름으로 변경하면 된다.
무조건 interrupt()
를 호출했다고 즉각 InterruptException
을 발생 시키는 것이 아니다.sleep()
과 같은 InterruptException
을 발생시키는 코드를 호출하거나 호출중이어야지만 예외가 발생하고, catch
블록에서 예외가 처리된다.
- 예를 들어 위 코드에서
while(true)
,log("작업 중")
에서는InterruptException
이 발생되지 않는다.
정리: trhead.interrupt()
-> 인터럽트 상태 true
-> 해당 스레드의 Thread.sleep()
실행 중이라면 -> InterruptException
예외 발생 -> 해당 예외가 발생되면 자동으로 인터럽트 상태 false
&& 해당 스레드 상태 TiIMED_WAITING
-> RUNNABLE
-> 예외 catch -> 정상 흐름
인터럽트가 true
일 경우에 인터럽트 예외가 발생되면, 해당 스레드는 실행 가능 상태가 되고, 인터럽트 상태도 false
가 된다.
덕분에 runFlag
을 적용한 이전 방식보다 훨씬 좋아졌다.
하지만 문제가 있다. sleep()
과 같은 메소드를 만날 때만 InterruptException
예외가 발생한다.
이를 해결하는 while(인터럽트 체크)
로 사용하는 방법은 아래와 같다.
스레드의 인터럽트 상태를 단순히 확인 하려면 isInterrupted()
를 사용하면 된다.
isInterrupt()
로 while
문을 체크하면 어떻게 될까?
thread.interrupt()
가 발생되어 인터럽트 상태가 `ture가 된다.isInterrupt()
로만while
문에 체크를 하면, 인터럽트의 상태는 바뀌지 않는다.- 인터럽트 상태가
false
로 바뀌지 않고,true
로 유지되기 때문에 그 뒤의while
문 밖의sleep()
과같은InterruptException
예외가 발생되는 메소드를 만나면 예외가 발생된다. - 자원 정리를 할 때
sleep()
같은 메소드를 만나면 심각한 문제로 이어진다.
하지만, 직접 체크해서 사용할 때는 Thread.interrupted()
를 사용하면 된다.
- 인터럽트 상태라면
true
를 반환하고 인터럽트 상태를false
로 반환한다. - 스레드가 인터럽트 상태가 아니라면
false
를 반환하고 인터럽트 상태를 변경하지 않는다.
import java.util.Queue;
import java.util.Scanner;
import java.util.concurrent.ConcurrentLinkedQueue;
import static util.MyLogger.log;
public class MyPrinterV3 {
public static void main(String[] args) {
Printer printer = new Printer();
Thread printerThread = new Thread(printer, "printer");
printerThread.start();
Scanner userInput = new Scanner(System.in);
while (true) {
log("프린터할 문서를 입력하세요. 종료 (q): ");
String input = userInput.nextLine();
if (input.equals("q")) {
printerThread.interrupt();
break;
}
printer.addJob(input);
}
}
static class Printer implements Runnable {
Queue<String> jobQueue = new ConcurrentLinkedQueue<>();
@Override
public void run() {
while (!Thread.interrupted()) {
if (jobQueue.isEmpty()) {
continue;
}
try {
String job = jobQueue.poll();
log("출력 시작: " + job + "대기 문서: " + jobQueue);
Thread.sleep(3000);
log("출력 완료");
} catch (InterruptedException e) {
log("인터럽트!");
break;
}
}
log("프린터 종료");
}
public void addJob(String input) {
jobQueue.offer(input);
}
}
}
yield - 양보
어떤 스레드를 얼마나 실행할지는 운영체제가 스케줄링을 통해 결정한다.
그런데 특정 스레드가 크게 바쁘지 않은 상황이어서 다른 스레드에 CPU 실행 기회를 양보할 수 있다.
이렇게 양보하면 스케줄링 큐에 대기 중인 다른 스레드가 CPU 실행 기회를 더 빨리 얻을 수 있다.
Thead.yield()
메서드는 현재 실행 중인 스레드가 자발적으로 CPU를 양보하여 다른 스레드가 실행될 수 있도록 한다.RUNNABLE
상태를 유지한다. 스케줄링 큐로 다시 들어가서 대기하기 때문이다.
단지 운영체제의 스케줄러에게 단지 힌트를 제공할 뿐, 강제적으로 실행 순서를 양보를 하지 않는다.
양보할 사람이 없다면 본인 스레드가 계속 실행될 수 있다.
while (!Thread.interrupted()) {
if (jobQueue.isEmpty()) {
Thread.yield(); // 추가
continue;
}
...
}