Thread
클래스는 스레드를 생성하고 관리하는 기능을 제공한다.
스레드를 생성할 때는 실행할 Runnable
인터페이스의 구현체와 스레드의 이름을 전달 할 수 있다.
Thread myThread = new Thread(new HelloRunnable(), "myThread");
스레드 이름: 스레드의 이름을 반환하는 메서드이다. 생성자에서 myThread
라는 이름을 지정해 그 값이 반환된다.
log("myThread.getName() = " + myThread.getName());
스레드 우선순위: 스레드의 우선순위를 반환하는 메서드이다. 우선순위는 1(가장 낮음)부터 10(가장 높음)까지의 값으로 설정이 가능하며, 기본값은 5이다. setPriority()
메서드를 사용해서 우선순위를 변경할 수 있다.
스레드 그룹 getThreadGroup()
: 스레드가 속한 스레드 그룹을 반환하는 메서드이다. 기본적으로 모든 스레드는 부모 스레드와 동일한 스레드 그룹에 속하게 된다.
myThread
는 main
스레드에 의해 생성되었으므로 main
스레드가 부모이다.스레드 상태 geteState()
: 스레드의 현재 상태를 반환하는 메서드이다. 반환되는 값은 Thread.State
열거형에 정의된 상수중 하나이다.
start()
메서드가 호출되지 않은 상태이다.start()
메서드가 호출되면 스레드는 이 상태로 들어간다.synchronized
블록에 진입하기 위해 락을 얻어야 하는 경우 이 상태에 들어간다.wait(),join()
메서드가 호출될때 이 상태이다.noify(),notifyAll()
을 호출하거나 join()
이 완료될 때까지 기다린다.sleep(long millis)
, wait(long timeout)
, join(long millis)
메서드가 호출될때 이 상태가 된다.Runnable
인터페이스의 run()
메서드를 구현할 때 체크 예외는 밖으로 던질 수 없다.
public interface Runnable {
void run();
}
자바에서 매서드를 재정의 할 때 부모 메서드가 체크 예외를 던지지 않는경우 재정의된 자식 메서드도 체크 예외를 던질 수 없다. 자식 메서드는 부모 메서드가 던지는 예외 하위만 던질 수 있다.
하지만 보시다시피 runnable
인터페이스는 예외를 던지지 않아 이를 상속받는 클래스들은 예외를 던질 수 없다.
waiting은 스레드가 다른 스레드의 특정 작업이 완료되기를 무기한 기다리는 상태이다.
public class JoinMainV0 {
public static void main(String[] args) {
log("Start");
Thread thread1 = new Thread(new Job(), "thread-1");
Thread thread2 = new Thread(new Job(), "thread-2");
thread1.start();
thread2.start();
log("End");
}
static class Job implements Runnable {
@Override
public void run() {
log("작업 시작"); sleep(2000); log("작업 완료");
}
}
}
위 구문 결과값을 출력하면 가끔 메인 메서드가 스레드의 결과값이 나오기도 전에 종료되는 경우가 있다.
메인 스레드는 다른 스레드에게 작업지시만 할 뿐이지 그들의 작업이 끝날때까지 기다리지 않는 점을 기억하자.
하지만 메인 스레드가 무조건 다른 스레드의 결과값이 나온 후에 종료할려면 join
을 사용해야한다.
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
이 코드는 스레드를 하나만 사용하기 때문에 CPU 코어도 하나만 사용할 수 있다. 코어를 더 효율적으로 사용하기 위해서는 여러 스레드로 나눠 계산하면 된다.
public class JoinMainV1 {
public static void main(String[] args) {
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("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 {
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);
}
}
}
좀 더 효율적이 될것이라 믿고 코드를 돌려보면 5050이 아닌 0이 나온다.
0이 나온 이유는 메인 스레드가 다른 스레드의 계산 결과를 기다리지않고 스레드를 종료시켜버려서 0이 나온 것이다.
이 문제를 해결하기 위해서는 스레드가 실행 후 sleep
을 걸어 timed-waiting
상태로 만들어 버릴 수도 있지만 이것보다 join()
메서드를 활용하면 문제가 더 쉽게 해결된다.
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 {
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);
}
}
}
main
에서 다음 코드를 실행하면 메인 스레드는 1,2 스레드가 종료될때 까지 기다린다. 이때 메인 스레드는 waiting
상태가 된다.
Waiting (대기 상태)
스레드가 다른 스레드의 특정 작업이 완료되기를 무기한 기다리는 상태이다.
join()
을 호출하는 스레드는 대상 스레드가 TERMINATED
상태가 될 때 까지 대기한다.
대상 스레드가 TERMINATED
상태가 되면 호출 스레드는 다시 RUNNABLE
상태가 되면서 다음 코드를 수행한다.
이렇듯 특정 스레드가 완료될 때 까지 기다려야 하는 상황이라면 join()
을 사용하면 된다
join()
: 호출 스레드는 대상 스레드가 완료될 때 까지 무한정 대기한다.join(ms)
: 호출 스레드는 특정 시간 만큼만 대기한다. 호출 스레드는 지정한 시간이 지나면 다시 RUNNABLE
interrupt
를 사용하면 Waiting,Timed_Waiting
같은 대기 상태의 스레드를 직접 깨워서 작동하는 Runnable
상태로 만들 수 있다.
InterruptedException
이 발생한다.Runnable
상태가 되고 코드를 정상 수행한다.InterruptedException
을 catch
로 잡아 정상 흐름으로 변경하면 된다.자바에서는 스레드가 인터럽트 상태인지 알려주는 isInterrupted()
메서드를 제공해 주기도 한다.
isInterrupted
는 인터럽트의 상태를 변경하는 것이 아닌 단순히 인터럽트의 상태를 확인만 한다.스레드의 인터럽트 상태를 직접 체크해서 사용할 때는 Thread.interrupted()
를 사용하면 된다.
true
를 반환하고 해당 스레드의 상태를 false
로 변경한다.false
를 반환하고 해당 스레드의 상태를 변경하지 않는다.public static void main(String[] args) throws InterruptedException {
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")) {
printer.work = false;
break;
}
printer.addJob(input);
}
}
static class Printer implements Runnable {
volatile boolean work = true;
Queue<String> jobQueue = new ConcurrentLinkedQueue<>();
@Override
public void run() {
while (work) {
if (jobQueue.isEmpty()) {
continue;
}
String job = jobQueue.poll();
log("출력 시작: " + job + ", 대기 문서: " + jobQueue);
sleep(3000); //출력에 걸리는 시간
log("출력 완료: " + job);
}
log("프린터 종료"); }
public void addJob(String input) {
jobQueue.offer(input);
}
main 스레드
: 사용자의 입력을 받아서 Printer
인스턴스의 jobQueue
에 담는다.Printer 스레드
: jobQueue
에 내용이 있으면 poll()
메서드를 이용해 내용을 꺼낸다.q
를 입력한 후 바로 종료되지 않는다는 점이다. public static void main(String[] args) throws InterruptedException {
Printer printer = new Printer();
Thread printerThread = new Thread(printer, "printer");
printerThread.start();
Scanner userInput = new Scanner(System.in);
while (true) {
System.out.println("프린터할 문서를 입력하세요. 종료 (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("출력 완료: " + job);
} catch (InterruptedException e) {
log("인터럽트!");
break;
}
}
log("프린터 종료");
}
Thread.interrupted()
메서드를 이용하면 해당 스레드가 인터럽트 상태인지 아닌지 확인할 수 있다.어떤 스레드를 얼마나 실행할지는 운영체제가 스케줄링을 통해 결정한다.그런데 특정 스레드가 바쁘지 않은 상황이어서 다른 스레드에 CPU 실행 기회를 양보하고 싶을 수 있다. 이렇게 양보하면 스큐줄링 큐에 대기 중인 다른 스레드가 CPU 실행기회를 더 빨리 얻을 수 있다.