Search
Duplicate
📒

[Java Study] 13-1. 스레드 생성, 생명주기, 제어 메서드

상태
완료
수업
Java Study
주제
연관 노트
3 more properties
참고

자바 스레드

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_WAITINGRUNNABLE 상태로 변경되고, 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
복사