참고
자바 스레드
NOTE
자바의 멀티 스레딩은 자바 프로그램에서 여러 스레드를 동시에 실행하여 병렬로 작업을 수행하는 기술입니다.
스레드 생성 후
자바에서 스레드를 생성하는 방법은 2가지 입니다. Thread 클래스를 상속받거나, Runnable 인터페이스를 구현하는 방법입니다. 대부분 Runnable을 구현해서 이루어 집니다.
public class HelloThread extends Thread {
@Override
public void run() {
// 현재 코드를 실행중인 스레드 이름조회
System.out.println(Thread.currentThread().getName() + ": run()");
}
}
Java
복사
1. Thread 상속
public class HelloThreadMain {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + " main() start");
HelloThread helloThread = new HelloThread();
helloThread.start(); // start()는 스레드를 실행하는 메서드
System.out.println(Thread.currentThread().getName() + " main() end");
}
}
Java
복사
public class HelloRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ": run()");
}
}
Java
복사
2. Runnable 구현
public class HelloRunnableMain {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + ": main() start");
HelloRunnable helloRunnable = new HelloRunnable();
Thread thread = new Thread(helloRunnable);
thread.start();
System.out.println(Thread.currentThread().getName() + ": main() end");
}
}
Java
복사
•
Thread.start()가 아닌, Thread.run()을 해버리면 생성된 스레드가 아닌 main 스레드가 run() 메서드를 호출하게 되므로 생성된 스레드는 아무일도 하지 않게됩니다.
각 스레드는 별개의 흐름으로 진행된다.
•
스레드는 동시에 실행되기 때문에 스레드 간의 실행 순서는 얼마든지 달라질 수 있습니다.
데몬 스레드(Daemon Thread)
NOTE
데몬 스레드는 백그라운드에서 실행되는 특별한 종류의 스레드입니다. 데몬 스레드는 일반(no-daemon) 스레드가 종료되면 자동으로 종료되는 것과 달리, 모든 스레드가 종료될 때까지 JVM이 계속 실행 상태를 유지하게 합니다.
public class DaemonThreadMain {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + " main() start");
DeamonThread deamonThread = new DeamonThread();
deamonThread.setDaemon(true); // 데몬 스레드 여부
deamonThread.start();
System.out.println(Thread.currentThread().getName() + " main() end");
}
static class DeamonThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ": run()");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + ": run() end");
}
}
}
Java
복사
•
setDaemon(true)를 설정하면 데몬 스레드를 사용할 수 있습니다.
데몬 스레드 용도
•
시스템 모니터링, 로그 수집, GC와 같이 주기적으로 실행되지만 애플리케이션의 주요 흐름에 영향을 미치지 않는 작업에 사용됩니다.
•
Main 스레드나, 다른 스레드가 필요로하는 서비스(네트워크 연결 유지, 데이터 동기화)를 제공합니다.
스레드 생명주기 / 제어
NOTE
Thread 클래스는 스레드의 상태, 우선순위, 이름, 스레드 그룹 등 다양한 정보를 가지고 있으며 스레드를 제어할 수 있는 다양한 메서드를 제공하고 있습니다.
public class ThreadInfoMain {
public static void main(String[] args) {
// main 스레드
Thread mainThread = Thread.currentThread();
// 스레드 정보
log("mainThread = " + mainThread);
// 스레드 ID
log("mainThread.threadId() = " + mainThread.threadId());
// 스레드 이름
log("mainThread.getName() = " + mainThread.getName());
// 우선순위
log("mainThread.getPriority() = " + mainThread.getPriority());
// 스레드 그룹
log("mainThread.getThreadGroup() = " + mainThread.getThreadGroup());
// 스레드 상태
log("mainThread.getState() = " + mainThread.getState());
// my 스레드
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());
log("myThread.getThreadGroup() = " + myThread.getThreadGroup());
log("myThread.getState() = " + myThread.getState());
}
}
Java
복사
•
threadId(): 스레드의 고유 식별자를 반환하는 메서드이며, 이 ID는 JVM내에서 각 스레드에 대해 유일합니다.
•
getName(): 스레드는 이름을 가질 수 있으며, 스레드 식별을 위해 사용합니다.
•
getPriority: 스레드는 우선순위를 가지며 1~10 범위까지 설정되고 기본값은 5입니다. 값이 클수록 우선순위가 큽니다.
•
getThreadGroup: 스레드는 그룹으로 묶어서 관리가 가능하며, 특정 그룹에 속한 모든 스레드를 한번에 제어할 수 있습니다.
•
getState(): 스레드의 상태를 나타내는 ENUM을 제공합니다.
스레드 생명주기
NOTE
Thread의 생명주기는 모두 Thread.State Enum으로 정의되어 있습니다.
Thread 상태
•
NEW (새로운 상태): 스레드가 생성되었으나 아직 시작되지 않음
•
Runnable(실행 가능 상태): 스레드가 실행 중 / 실행 될 준비가 된 상태
◦
그림의 Runnable, Running은 동일한 상태로 보면된다.
•
일시 중지 상태들
◦
Blocked(차단 상태): 스레드가 동기화 락을 기다리는 상태입니다. 인터럽트가 걸려도 대기 상태에서 빠져나올 수 없습니다.
◦
Waiting(대기 상태): 스레드가 무기한으로 다른 스레드의 작업을 기다리는 상태입니다. 인터럽트가 걸리면 InterruptedException이 발생합니다.
◦
Timed Waiting(시간 제한 대기 상태): 스레드가 일정 시간 동안 다른 작업을 기다리는 상태입니다. Waiting과 거의 동일합니다.
•
Terminated(종료 상태): 스레드의 실행이 완료된 상태
조인(join) - 작업 대기
NOTE
join() 메서드는 하나의 스레드가 다른 스레드의 작업이 끝날 때까지 기다리도록 만들 때 사용합니다. 즉 join()을 호출한 스레드는 대상 스레드가 종료될 때까지 실행을 중지(WAITING)합니다.
join() 메서드가 필요한 이유에 대해서 알아봅시다.
만약 1~100까지를 더하는 작업을 진행하는데 스레드 2개로, 1~50, 51~100의 합을 각각 계산하고 마지막에 합치는 시나리오를 진행해보겠습니다.
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("작업 시작");
sleep(2000);
int sum = 0;
for (int i = startValue; i < endValue; i++) {
sum += i;
}
result = sum;
log("작업 완료 result = " + result);
}
}
JavaScript
복사
연산 코드(연산은 2초가 걸린다 가정)
public class JoinMainV1 {
public static void main(String[] args) {
log("Start");
// thread 1 => 1~50
// thread 2 => 51~100
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("task1.result = " + task1.result);
log("task2.result = " + task2.result);
// thread 1과 thread 2의 결과를 합친다.
int sumAll = task1.result + task2.result;
// 값은 0이 나온다. (비정상)
log("task1 + task2 = " + sumAll);
log("End");
}
}
Java
복사
값이 제대로 나오지 않는 이유
•
main 스레드는 두 스레드를 시작한 다음 바로 result를 조회하기 때문에, 계산 이전의 값을 조회하여 0이 나옵니다.
•
여기서 핵심은 main 스레드가 스레드 1~2의 계산이 종료될 때까지 기다려야 한다는 점입니다.
•
sleep()을 사용해 타이밍을 맞추거나, while문을 통해 지속적으로 상태를 확인하는 방법도 있지만, Java에서는 join()으로 깔끔하게 이러한 문제를 해결할 수 있습니다.
public class JoinMainV4 {
// join의 경우 InterruptedException을 던져줘야 한다.
public static void main(String[] args) throws InterruptedException {
log("Start");
// thread 1 => 1~50
// thread 2 => 51~100
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();
// 대기 시간을 조정할수도 있다.
// thread1.join(2000);
// thread2.join(2000);
log("join() - main 스레드 대기완료");
log("task1.result = " + task1.result);
log("task2.result = " + task2.result);
// task1 + task2 = 5050(성공)
int sumAll = task1.result + task2.result;
log("task1 + task2 = " + sumAll);
log("End");
}
}
Java
복사
join() 메서드로 결과값 대기후 조회
인터럽트(Interrupt) - 작업 포기 & 대기 해제
NOTE
인터럽트는 스레드가 일시 중지된 상태에서 중단되거나, 현재의 작업을 포기하고 다른 작업을 수행하도록 요청받았을 때 처리하는 방법입니다.
인터럽트를 통해서, 외부에서 해당 스레드를 종료시키거나, 특정 리소스를 점유하는 스레드를 중단하고 해제할 수 있습니다.
Thread.interrupt(), Thread.isInterrupted()
static class MyTask implements Runnable {
@Override
public void run() {
try {
// while문으로 무한작업
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());
}
}
}
Java
복사
public class ThreadStopMainV2 {
public static void main(String[] args) {
MyTask task = new MyTask();
Thread thread = new Thread(task, "work");
thread.start();
sleep(4000);
log("작업 중단 지시 runFlag=false");
// 스레드 인터럽트 발생
thread.interrupt();
log("work 스레드 인터럽트 상태1 = " + thread.isInterrupted());
}
}
Java
복사
스레드가 인터럽트 상태(true)일 때 sleep과 같이 InterruptedException이 발생하는 메서드를 호출하거나 이미 호출하고 대기 중이라면 예외가 발생합니다. 이때 2가지 일이 발생합니다.
•
work 스레드는 TIMED_WAITING ⇒ RUNNABLE 상태로 변경되고, InterruptedException 예외를 처리하면서 반복문을 탈출한다.
•
인터럽트 상태에서 인터럽트 예외가 발생하면, work 스레드는 다시 RUNNABLE 상태가 된다.
하지만 위의 코드에서 아쉬운점이 있다면, sleep()인 상태에서만 인터럽트가 발생한다는 점이다. while문에 인터럽트 상태를 두면 더 간단하게 구현할 수 있을거 같다. 하지만 여기서 주의해야할 점이 있다.
public class ThreadStopMainV3 {
public static void main(String[] args) {
MyTask task = new MyTask();
Thread thread = new Thread(task, "work");
thread.start();
sleep(100);
log("작업 중단 지시 thread.interrupt()");
thread.interrupt();
// work 스레드 인터럽트 상태1 = false
log("work 스레드 인터럽트 상태1 = " + thread.isInterrupted());
}
static class MyTask implements Runnable {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) { // 인터럽트 상태 변경x
log("작업 중");
}
// work 스레드 인터럽트 상태2 = true(문제점)
log("work 스레드 인터럽트 상태2 = " + Thread.currentThread().isInterrupted());
try{
log("자원 정리");
Thread.sleep(1000);
log("자원 종료");
} catch (InterruptedException e) {
log("자원 정리 실패 - 자원 정리 중 인터럽트 발생");
log("work 스레드 인터럽트 상태3 = " + Thread.currentThread().isInterrupted());
}
log("작업 종료");
}
}
}
Java
복사
while문을 탈출하고 나서도 인터럽트 상태가 true이다.
•
work 스레드의 인터럽트 상태가 계속해서 true로 유지된다. 만약 이후에 InterruptedException을 던지는 메서드를 사용하게 되면, 예상하지 못한 동작을 할 수 있다.
•
인터럽트의 목적을 달성하면 다시 정상으로 돌려두어야 한다.
Thread.interrupted()
Thread 클래스에는 위와 같은 문제를 해결하기 위해 Thread.interrupted() 함수를 제공한다.
•
스레드가 인터럽트 상태라면 true를 반환하고, 인터럽트 상태를 false로 변경한다.
•
스레드가 인터럽트 상태가 아니라면 false를 반환하고, 상태를 유지한다.
public class ThreadStopMainV4 {
public static void main(String[] args) {
MyTask task = new MyTask();
Thread thread = new Thread(task, "work");
thread.start();
sleep(100);
log("작업 중단 지시 thread.interrupt()");
thread.interrupt();
log("work 스레드 인터럽트 상태1 = " + thread.isInterrupted());
}
static class MyTask implements Runnable {
@Override
public void run() {
// 인터럽트 상태 변경 O
while (!Thread.interrupted()) {
log("작업 중");
}
// work 스레드 인터럽트 상태2 = false(성공!)
log("work 스레드 인터럽트 상태2 = " + Thread.currentThread().isInterrupted());
try{
log("자원 정리");
Thread.sleep(1000);
log("자원 종료");
} catch (InterruptedException e) {
log("자원 정리 실패 - 자원 정리 중 인터럽트 발생");
log("work 스레드 인터럽트 상태3 = " + Thread.currentThread().isInterrupted());
}
log("작업 종료");
}
}
}
Java
복사
yield - 작업 양보
NOTE
Thread.yield() 메서드는 현재 실행 중인 스레드가 실행을 잠시 멈추고, 같은 우선순위를 가진 다른 스레드에게 실행 기회를 양보하도록 요청하는 역할을 합니다. 단 yiled()는 기회만 주고 어떤 스레드가 양보받을지는 스케줄러가 정합니다.
자바의 스레드가 RUNNABLE 상태일 때, 운영체제의 스케줄링은 다음과 같은 상태를 가집니다.
•
실행 상태(Running): 스레드가 실제로 CPU에서 실행 중이다.
•
실행 대기 상태(READY): 스레드가 실행될 준비가 되었지만, CPU가 바빠서, 스케줄링 큐에서 대기중이다.
Thread.yiled는 현재 실행중인 스레드가 자발적으로 CPU를 양보해 다른 스레드가 실행될 수 있도록 합니다. 즉 자신에게 할당된 실행 시간을 포기하고 다른 스레드에게 실행 기회를 줍니다.
•
만약 yeild()를 실행해도 양보할 스레드가 없다면 본인이 계속해서 실행됩니다.
static class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " - " + i);
// 1. empty
// sleep(1); // 2. sleep
Thread.yield(); // 3. yield
}
}
}
Java
복사
public class YieldMain {
static final int THREAD_COUNT = 1000;
public static void main(String[] args) {
// 1000개 쓰레드 실행
for (int i = 0; i < THREAD_COUNT; i++) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}
}
}
Java
복사