Preface
이번 장에선 스레드와 관련된 내용을 공부했다.
앞 부분도 생각보다 복잡하고 어려워서 당황했는데, 마지막 부분에 있는 스레드 그룹과 스레드풀 쪽은 솔직히 이해가 잘 되지 않아 한 번 훑어보는 식으로 넘어갔다.
아무래도 아직 공부하지 않은 제네릭과 다양한 타입의 객체가 코드에 포함되어 있어서 그런 것 같다.
모든 내용을 이해하려고 시간을 쓰기 보단 이런 내용이 있구나 정도로 이해한 뒤, 추후 해당 내용이 필요할 때 다시 공부하는 것이 훨씬 효율적일 것 같다.
스레드 객체를 생성하는 방법 중 굳이 코드의 길이가 길어지는 Runnable 인터페이스 구현 방법을 사용하는 이유가 궁금해 chat-gpt에게 물어봤다.
Q: 두 방법 중 익명객체를 생성해 Thread 클래스를 상속받는 방법이 훨씬 코드도 짧고 간단한데, Runnable 인터페이스를 구현하는 방법을 권장하는 이유가 뭐야?
A: 익명 객체를 생성해 Thread 클래스를 상속받는 방법은 코드의 길이가 짧고 간단하기 때문에, 간단한 스레드 작업을 수행할 때는 편리합니다. 그러나 보다 복잡한 스레드 작업을 수행하는 경우, Runnable 인터페이스를 구현하는 방법이 더욱 권장됩니다.
그 이유는 다음과 같습니다.
1. 다중 상속 불가능: 자바에서는 클래스가 다른 클래스를 상속하는 다중 상속을 허용하지 않습니다. Thread 클래스를 상속한 클래스를 정의하면, 이미 상속하고 있는 다른 클래스가 있을 경우 스레드를 생성하기 위해 추가적인 클래스를 정의해야 합니다. 이는 클래스의 복잡도를 높일 수 있습니다.
2. 코드 재사용성: Runnable 인터페이스를 구현하는 방법은 클래스가 다른 클래스를 상속하지 않아도 되므로, 코드 재사용성이 높아집니다. Runnable 인터페이스를 구현하는 클래스는 다른 클래스에서도 사용될 수 있으며, 이는 객체 지향 프로그래밍의 기본 개념 중 하나인 코드 재사용성을 보장합니다.
3. 스레드 풀: Runnable 인터페이스를 구현하는 방법은 스레드 풀을 구현하는 데에도 적합합니다. 스레드 풀은 스레드를 미리 생성해 놓고 필요할 때마다 스레드를 가져와 작업을 처리하는 방식으로 동작합니다. 이 때 스레드를 생성하는 비용이 줄어들기 때문에, Runnable 인터페이스를 구현하는 방법이 스레드 풀 구현에 더 적합합니다.
따라서, 간단한 스레드 작업을 수행하는 경우에는 Thread 클래스를 상속받는 방법이 간편할 수 있지만, 보다 복잡한 스레드 작업을 수행하거나, 코드의 재사용성을 고려해야 하는 경우에는 Runnable 인터페이스를 구현하는 방법을 권장합니다.
1. 멀티 스레드 개념
- 프로세스(process): 실행 중인 하나의 애플리케이션
→ 하나의 앱은 다중 프로세스를 만들기도 한다.
- 멀티 태스킹(multi tasking): 두 가지 이상의 작업을 동시에 처리하는 것
1) 멀티 태스킹이 꼭 멀티 프로세를 뜻하는 것은 아니다.
2) 한 프로세스 내에서 멀티 태스킹을 할 수 있도록 만들어진 애플리케이션들도 있다.
- 스레드(thread): CPU가 처리할 수 있는 작업의 단위
- 멀티 프로세스: 애플리케이션 단위의 멀티 태스킹
1) 여러 개의 애플리케이션을 동시 실행하는 것
2) 서로 독립적이므로 하나의 프로세스에서 오류가 발생해도 다른 프로세스에 영향을 미치지 않는다.
- 멀티 스레드: 애플리케이션 내부에서의 멀티 태스킹
1) 여러 개의 스레드를 하나의 프로세스 내에서 동시 실행하는 것
2) 하나의 프로세스 내부에 생성되므로 다른 스레드에게 영향을 미친다.
- 메인 스레드가 작업 스레드보다 먼저 종료되어도 작업 스레드가 계속 실행 중이라면 프로세스는 종료되지 않는다.
2. 작업 스레드 생성과 실행
- 자바에서는 작업 스레드도 객체로 생성되므로 클래스가 필요하다.
1) java.lang.Thread 클래스를 직접 객체화해서 생성할 수 있다.
2) Thread를 상속한 하위 클래스를 만들어 생성할 수 있다.
- java.lang.Thread 클래스로부터 작업 스레드 객체를 직접 생성하려면 Runnable을 매개값으로 갖는 생성자를 호출해야 한다.
Thread thread = new Thread(Runnable target);
- Runnable 인터페이스
1) 작업 스레드가 실행할 수 있는 코드를 가지고 있는 객체라는 의미이다.
2) 인터페이스 타입이므로 구현 객체를 만들어 대입해야 한다.
3) run( ) 메소드 하나가 정의되어 있으므로 구현 클래스는 이를 재정의하여 작업 스레드가 실행할 코드를 작성해야 한다.
class Task implements Runnable {
public void run() {
//스레드가 실행할 코드;
}
}
- 작업 스레드 생성 방법: Runnable 구현 객체를 생성한 후, 이것을 매개값으로 Thread 생성자를 호출
Runnable task = new Task();
Thread thread = new Thread(task);
→ 코드를 절약하기 위해 Thread 생성자를 호출할 때 Runnable 익명 객체를 매개값으로 사용할 수 있다.
Thread thread = new Thread(new Runnable() {
public void run() {
//스레드가 실행할 코드;
}
});
- 작업 스레드는 생성 즉시 실행되는 것이 아니라 start( ) 메소드를 호출해야 실행된다.
thread.start();
- 메인 스레드만 이용한 경우
package ch12;
import java.awt.Toolkit;
public class BeepPrintExample1 {
public static void main(String[] args) {
Toolkit toolkit = Toolkit.getDefaultToolkit();
for (int i = 0; i < 5; i++) {
toolkit.beep();
try {
Thread.sleep(500);
} catch (Exception e) {
}
}
for (int i = 0; i < 5; i++) {
System.out.println("띵");
try {
Thread.sleep(500);
} catch (Exception e) {
}
}
}
}
- Runnable 인터페이스를 사용해 작업 스레드를 생성한 경우
package ch12;
import java.awt.Toolkit;
class BeepTask2 implements Runnable {
public void run() {
Toolkit toolkit = Toolkit.getDefaultToolkit();
for (int i = 0; i < 5; i++) {
toolkit.beep();
try {
Thread.sleep(500);
} catch (Exception e) {
}
}
}
}
public class BeepPrintExample2 {
public static void main(String[] args) {
Runnable beepTask = new BeepTask2();
Thread thread = new Thread(beepTask);
thread.start();
for (int i = 0; i < 5; i++) {
System.out.println("띵");
try {
Thread.sleep(500);
} catch (Exception e) {
}
}
}
}
package ch12;
import java.awt.Toolkit;
public class AnonymousRunnableBeepPrintExample {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() { // 익명 객체에서 곧바로 Runnable 객체를 생성한 후 메소드를 재정의하여 실행
@Override
public void run() {
Toolkit toolkit = Toolkit.getDefaultToolkit();
for (int i = 0; i < 5; i++) {
toolkit.beep();
try {
Thread.sleep(500);
} catch (Exception e) {
}
}
}
});
thread.start();
for (int i = 0; i < 5; i++) {
System.out.println("띵");
try {
Thread.sleep(500);
} catch (Exception e) {
}
}
}
}
- Thread의 하위 클래스로 작업 스레드 클래스를 정의하는 방법: Thread 클래스를 상속한 후 run 메소드를 재정의하여 스레드가 실행할 코드를 작성한다.
public class WorkerThread extends Thread {
@Override
public void run() {
//스레드가 실행할 코드
}
}
Thread thread = new WorkerThread();
→ 코드를 절약하기 위해 Thread 익명 객체로 작업 스레드 객체를 생성할 수도 있다.
Thread thread = new Thread)( {
public void run() {
//스레드가 실행할 코드;
}
};
- Thread의 하위 클래스로 작업 스레드를 정의한 경우
package ch12;
import java.awt.Toolkit;
class BeepTask3 extends Thread {
public void run() {
Toolkit toolkit = Toolkit.getDefaultToolkit();
for (int i = 0; i < 5; i++) {
toolkit.beep();
try {
Thread.sleep(500);
} catch (Exception e) {
}
}
}
}
public class BeepPrintExample3 {
public static void main(String[] args) {
Thread thread = new BeepTask3();
thread.start();
for (int i = 0; i < 5; i++) {
System.out.println("띵");
try {
Thread.sleep(500);
} catch (Exception e) {
}
}
}
}
package ch12;
import java.awt.Toolkit;
public class AnonymousExtendsBeepPrintExample {
public static void main(String[] args) {
Thread thread = new Thread() { // Thread의 하위 클래스로 작업 스레드를 정의
@Override
public void run() {
Toolkit toolkit = Toolkit.getDefaultToolkit();
for (int i = 0; i < 5; i++) {
toolkit.beep();
try {
Thread.sleep(500);
} catch (Exception e) {
}
}
}
};
thread.start();
for (int i = 0; i < 5; i++) {
System.out.println("띵");
try {
Thread.sleep(500);
} catch (Exception e) {
}
}
}
}
- 메인 스레드는 "main"이라는 이름을, 직접 생성한 스레드는 "Thread-n"이라는 이름으로 설정된다.
1) "n"은 스레드의 번호를 의미하며 0부터 카운트된다.
2) setName( ) 메소드: 다른 이름으로 설정
thread.setName("스레드 이름");
3) getName( ) 메소드: 스레드의 이름을 얻음
thread.getName();
4) 위의 두 메소드는 Thread의 인스턴스 메소드이므로 스레드 객체의 참조가 필요하다.
→ 스레드 객체의 참조를 가지고 있지 않다면 Thread의 정적 메소드인 currentThread( )로 코드를 실행하는 현재 스레드의 참조를 얻을 수 있다.
Thread thread = Thread.currentThread();
package ch12;
class ThreadA extends Thread {
public ThreadA() {
setName("ThreadA");
}
public void run() {
for (int i = 0; i < 2; i++) {
System.out.println(getName() + "가 출력한 내용");
}
}
}
class ThreadB extends Thread {
public void run() {
for (int i = 0; i < 2; i++) {
System.out.println(getName() + "가 출력한 내용");
}
}
}
public class ThreadNameExample {
public static void main(String[] args) {
Thread mainThread = Thread.currentThread();
System.out.println("프로그램 시작 스레드 이름: " + mainThread.getName());
ThreadA threadA = new ThreadA();
System.out.println("작업 스레드 이름: " + threadA.getName());
threadA.start();
ThreadB threadB = new ThreadB();
System.out.println("작업 스레드 이름: " + threadB.getName());
threadB.start();
}
}
3. 스레드 우선순위
- 동시성(Concurrency): 멀티 작업을 위해 '하나의 코어'에서 '멀티 스레드'가 번갈아가며 실행하는 성질
- 병렬성(Parallelism): 멀티 작업을 위해 '멀티 코어'에서 '개별 스레드'를 동시에 실행하는 성질
- 스레드 스케줄링: 스레드를 어떤 순서에 의해 동시성으로 실행할 것인가를 결정하는 것
1) 우선순위(Priority) 방식: 우선순위가 높은 스레드가 실행 상태를 더 많이 가지도록 스케줄링하는 방식
2) 순환 할당(Round-Robin) 방식: 시간 할당량(Time Slice)을 정해서 하나의 스레드를 정해진 시간만큼 실행한 후 다시 다른 스레드를 실행하는 방식
- 우선순위 방식은 개발자가 코드로 제어할 수 있다.
1) 우선순위는 1에서 10까지 부여: 1이 가장 낮은 우선순위, 10이 가장 높은 우선순위
2) 디폴트 값은 5이다.
3) setPriority( ) 메소드: 우선순위를 변경한다.
thread.setPriority(우선순위);
//매개값으로 Thread 클래스의 상수를 사용할 수도 있다.
thread.setPriority(Thread.MAX_PRIORITY);
thread.setPriority(Thread.NORM_PRIORITY);
thread.setPriority(Thread.MIN_PRIORITY);
4. 동기화 메소드와 동기화 블록
- 공유 객체를 동시에 사용해 변수의 값이 예상치 못한 값으로 변질되는 경우
package ch12;
class Calculator {
private int memory;
public int getMemory() {
return memory;
}
public synchronized void setMemory(int memory) {
this.memory = memory;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {}
System.out.println(Thread.currentThread().getName() + ": " + this.memory);
}
}
class User1 extends Thread {
private Calculator calculator;
public void setCalculator(Calculator calculator) {
this.setName("User1");
this.calculator = calculator;
}
public void run() {
calculator.setMemory(100);
}
}
class User2 extends Thread {
private Calculator calculator;
public void setCalculator(Calculator calculator) {
this.setName("User2");
this.calculator = calculator;
}
public void run() {
calculator.setMemory(50);
}
}
public class MainThreadExample {
public static void main(String[] args) {
Calculator calculator = new Calculator();
User1 user1 = new User1();
user1.setCalculator(calculator);
user1.start();
User2 user2 = new User2();
user2.setCalculator(calculator);
user2.start();
}
}
- 임계 영역(critical section): 멀티 스레드 프로그램에서 단 하나의 스레드만 실행할 수 있는 코드 영역
- 자바에선 임계 영역 지정을 위해 동기화(syncronized) 메소드와 동기화 블록을 제공해 스레드가 객체 내부의 동기화 메소드 또는 동기화 블록에 들어가면 즉시 객체에 잠금을 걸어 다른 스레드가 임계 영역 코드를 실행하지 못하도록 한다.
1) 메소드 선언에 syncronized 키워드를 붙이면 된다. (인스턴스와 정적 메소드 어디든 가능)
public syncronized void method() {
//단 하나의 스레드만 실행
};
2) 동기화 블록을 사용해 일부 내용만 임계 영역으로 만들 수도 있다.
public void method () {
//여러 스레드가 실행 가능한 영역
syncronized(공유객체: 공유 객체가 자기 자신이면 this를 넣을 수 있다.) {
//단 하나의 스레드만 실행
}
//여러 스레드가 실행 가능한 영역
}
5. 스레드 상태
- 스레드 객체를 생성한 후 start( ) 메소드를 호출하면 스레드는 실행 대기 상태가 된다.
- 실행 상태에서 일시 정지 상태로 가기도 하는데, 이는 스레드가 실행할 수 없는 상태이다.
→ 스레드가 다시 실행 상태로 가기 위해서는 일시 정지 상태에서 실행 대기 상태로 가야 한다.
- getState( ) 메소드: 스레드의 상태를 확인
상태 | 열거 상수 | 설명 |
객체 생성 | NEW | 스레드 객체가 생성, 아직 start( ) 메소드가 호출되지 않은 상태 |
실행 대기 | RUNNABLE | 실행 상태로 언제든지 갈 수 있는 상태 |
일시 정지 | WAITING | 다른 스레드가 통지할 때까지 기다리는 상태 |
TIMED_WAITING | 주어진 시간 동안 기다리는 상태 | |
BLOCKED | 사용하고자 하는 객체의 락이 풀릴 때까지 기다리는 상태 | |
종료 | TERMINATED | 실행을 마친 상태 |
package ch12;
class StatePrintThread extends Thread {
private Thread targetThread;
public StatePrintThread(Thread targetThread) {
this.targetThread = targetThread;
}
public void run() {
while (true) {
Thread.State state = targetThread.getState();
System.out.println("타겟 스레드 상태: " + state);
if (state == Thread.State.NEW) {
targetThread.start();
}
if (state == Thread.State.TERMINATED) {
break;
}
try {
Thread.sleep(500);
} catch (Exception e) {
}
}
}
}
class TargetThread extends Thread {
public void run() {
for (long i = 0; i < 1000000000; i++) {
}
try {
Thread.sleep(1500);
} catch (Exception e) {
}
for (long i = 0; i < 1000000000; i++) {
}
}
}
public class ThreadStateExample {
public static void main(String[] args) {
StatePrintThread statePrintThread = new StatePrintThread(new TargetThread());
statePrintThread.start();
}
}
6. 스레드 상태 제어
- 스레드 상태 제어: 실행 중인 스레드의 상태를 변경하는 것
- 스레드의 상태 변화를 가져오는 메소드
메소드 | 설명 |
interrupt( ) | 일시 정지 상태의 스레드에서 InterruptedException 예외를 발생시켜 예외 처리 코드에서 실행 대기 상태로 가거나 종료 상태로 갈 수 있도록 한다. |
notify( ) notifyAll( ) |
동기화 블록 내에서 wait( ) 메소드에 의해 일시 정지 상태에 있는 스레드를 실행 대기 상태로 만든다. (notify는 하나의 스레드만, notifyAll은 모든 스레드를: 동기화 메소드 혹은 동기화 블록 내에서만 사용 가능) |
suspend( ) 메소드에 의해 일시 정지 상태에 있는 스레드를 실행 대기 상태로 만든다. | |
sleep(long millis) sleep(long millis, int nanos) |
주어진 시간 동안 스레드를 일시 정지 상태로 만든다. 주어진 시간이 지나면 자동적으로 실행 대기 상태가 된다. (InterruptedException 예외 처리 필요) |
join( ) join(long millis) join(long millis, int nanos) |
join( ) 메소드를 호출한 스레드는 일시 정지 상태가 된다. 실행 대기 상태로 가려면 join( ) 메소드를 멤버로 가지는 스레드가 종료되거나 매개값으로 주어진 시간이 지나야 한다. |
wait( ) wait(long millis) wait(long millis, int nanos) |
동기화 블록 내에서 스레드를 일시 정지 상태로 만든다. 매개값으로 주어진 시간이 지나면 자동적으로 실행 대기 상태가 된다. 시간이 주어지지 않으면 notify( ), nofityAll( ) 메소드에 의해 실행 대기 상태로 갈 수 있다. |
스레드를 일시 정지 상태로 만든다. resume( ) 메소드를 호출하면 다시 실행 대기 상태가 된다. | |
yield( ) | 실행 중에 우선순위가 동일한 다른 스레드에게 실행을 양보하고 실행 대기 상태가 된다. |
스레드를 즉시 종료시킨다. |
- notify( ) 메소드 사용
package ch12;
class DataBox {
private String data;
public synchronized String getData() {
if (this.data == null) {
try {
wait();
} catch (InterruptedException e) {
}
}
String returnValue = data;
System.out.println("ConsummerThread가 읽은 데이터: " + returnValue);
data = null;
notify();
return returnValue;
}
public synchronized void setData(String data) {
if (this.data != null) {
try {
wait();
} catch (InterruptedException e) {
}
}
this.data = data;
System.out.println("ProducerThread가 생성한 데이터: " + data);
notify();
}
}
class ProducerThread extends Thread {
private DataBox dataBox;
public ProducerThread(DataBox dataBox) {
this.dataBox = dataBox;
}
@Override
public void run() {
for (int i = 1; i <= 3; i++) {
String data = "Data-" + i;
dataBox.setData(data);
}
}
}
class ConsumerThread extends Thread {
private DataBox dataBox;
public ConsumerThread(DataBox dataBox) {
this.dataBox = dataBox;
}
@Override
public void run() {
for (int i = 1; i <= 3; i++) {
String data = dataBox.getData();
}
}
}
public class WaitNotifyExample {
public static void main(String[] args) {
DataBox dataBox = new DataBox();
ProducerThread producerThread = new ProducerThread(dataBox);
ConsumerThread consumerThread = new ConsumerThread(dataBox);
producerThread.start();
consumerThread.start();
}
}
- stop( ) 메소드는 메소드를 강제로 종료하므로 좋은 방법이 아니다.
→ stop 플래그를 이용해 run( ) 메소드가 종료되도록 유도해야 한다.
package ch12;
class PrintThread1 extends Thread {
private boolean stop; // boolean default value is "false"
public void setStop(boolean stop) {
this.stop = stop;
}
public void run() {
while (!stop) {
System.out.println("실행 중");
}
System.out.println("자원 정리");
System.out.println("실행 종료");
}
}
public class StopFlagExample {
public static void main(String[] args) {
PrintThread1 printThread = new PrintThread1();
printThread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
printThread.setStop(true);
}
}
- interrupt( ) 메소드 사용
1) 스레드가 일시 정지 상태가 되지 않으면 interrupt( ) 메소드는 아무런 의미가 없다.
2) interrupted( ), isInterrupted( ) 메소드를 통해 interrupt( ) 메소드의 호출 여부를 알 수 있다.
package ch12;
class PrintThread2 extends Thread {
public void run() {
try {
while (true) {
System.out.println("run");
Thread.sleep(1);
}
} catch (InterruptedException e) {
}
System.out.println("자원 정리");
System.out.println("실행 종료");
}
}
public class InterruptExample {
public static void main(String[] args) {
Thread thread = new PrintThread2();
thread.start();
try {
Thread.sleep(1000); //thread2를 실행한 후 1초 뒤에 인터럽트 발생시킴
} catch (InterruptedException e) {
}
thread.interrupt();
}
}
7. 데몬 스레드
- 데몬(daemon) 스레드: 주 스레드의 작업을 돕는 보조적인 역할을 수행하는 스레드
→ 주 스레드가 종료되면 데몬 스레드는 강제적으로 자동 종료된다.
- setDaemon(true) 메소드: 스레드를 데몬으로 만든다.
1) start( ) 메소드 호출 전에 호출해야 한다.
2) isDaemon( ) 메소드를 통해 데몬 스레드인지 아닌지 구별할 수 있다.
package ch12;
class AutoSaveThread extends Thread {
public void save() {
System.out.println("작업 내용을 저장함");
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
break;
}
save();
}
}
}
public class DaemonExample {
public static void main(String[] args) {
AutoSaveThread autoSaveThread = new AutoSaveThread();
autoSaveThread.setDaemon(true);
autoSaveThread.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
}
System.out.println("메인 스레드 종료");
}
}
8. 스레드 그룹
- 스레드 그룹: 관련된 스레드를 묶어서 관리할 목적으로 이용
→ 스레드는 반드시 하나의 스레드 그룹에 포함되는데, 명시적으로 스레드 그룹에 포함시키지 않으면 자신을 생성한 스레드와 같은 스레드 그룹에 속하게 된다.
- 현재 스레드가 속한 스레드 그룹의 이름을 얻는 방법
ThreadGroup group = Thread.currentThread().getThreadGroup();
String groupName = group.getName();
- Thread의 정적 메소드인 getAllStackTraces( )를 이용하면 프로세스 내에서 실행하는 모든 스레드에 대한 정보를 얻을 수 있다.
→ Map 타입의 객체를 리턴: 키는 스레드 객체, 값은 스레드의 상태 기록을 갖고 있는 StackTraceElement[ ] 배열
Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();
package ch12;
import java.util.Map;
import java.util.Set;
public class ThreadInfoExample {
public static void main(String[] args) {
AutoSaveThread autoSaveThread = new AutoSaveThread();
autoSaveThread.setName("AutoSaveThread");
autoSaveThread.setDaemon(true);
autoSaveThread.start();
Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();
Set<Thread> threads = map.keySet();
for (Thread thread : threads) {
System.out.println("Name: " + thread.getName() + ((thread.isDaemon()) ? "(데몬)" : "(주)"));
System.out.println("\t" + "소속그룹: " + thread.getThreadGroup().getName());
System.out.println();
}
}
}
- ThreadGroup 객체를 생성하여 명시적으로 스레드 그룹을 만들 수도 있다.
ThreadGroup tg = new ThreadGroup(String name);
ThreadGroup tg = new ThreadGroup(ThreadGroup parent, String name);
//부모(parent) 스레드 그룹을 지정하지 않으면 현재 스레드가 속한 그룹의 하위 그룹으로 생성된다.
- 새로운 스레드 그룹을 생성한 후 그룹에 스레드를 포함시키려면 Thread 객체를 생성할 때 생성자 매개값으로 스레드 그룹을 지정하면 된다.
Thread t = new Thread(ThreadGroup group, Runnable target);
Thread t = new Thread(ThreadGroup group, Runnable target, String name);
Thread t = new Thread(ThreadGroup group, Runnable target, String name, long stackSize);
Thread t = new Thread(ThreadGroup group, String name);
//Runnable 타입의 target은 Runnable 구현 객체
- 스레드 그룹에서 제공하는 interrupt( ) 메소드를 이용하면 그룹에 포함된 모든 스레드들을 일괄 interrupt할 수 있다.
→ 개별 스레드에서 발생하는 InterruptedException에 대한 예외 처리는 개별적으로 해야 한다.
9. 스레드풀
- 스레드풀: 작업 처리에 사용되는 스레드를 제한된 개수만큼 정해 놓고 작업 큐(Queue)에 들어오는 작업들을 하나씩 스레드가 맡아 처리한다.
→ 작업 처리가 끝난 스레드는 다시 작업 큐에서 새로운 작업을 가져와 처리한다.
- ExecutorService 구현 객체는 Executors 클래스의 두 가지 메소드 중 하나를 이용해 생성할 수 있다.
1) 초기 스레드 수: ExecutorService 객체가 생성될 때 기본적으로 생성되는 스레드 수
2) 코어 스레드 수: 스레드 수가 증가된 후 사용되지 않는 스레드를 스레드풀에서 제거할 때 최소한 유지해야 할 스레드 수
메소드명 | 초기 스레드 수 | 코어 스레드 수 | 최대 스레드 수 |
newCachedThreadPool( ) | 0 | 0 | Integer.MAX_VALUE |
newFixedThreadPool(int n Threads) | 0 | nThreads | nThreads |
- newCachedThreadPool( ): 1개 이상의 스레드가 추가되었을 경우 60초 동안 추가된 스레드가 아무 작업을 하지 않으면 추가된 스레드를 종료하고 풀에서 제거한다.
ExecutorService executorService = Executors.newCachedThreadPool();
- newFixedThreadPool(int n Threads): 스레드가 작업을 처리하지 않고 놀고 있더라고 스레드 개수가 줄지 않는다.
ExecutorService executorService = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors()
);
- 코어 스레드 개수와 최대 스레드 개수를 직접 설정하는 방법
ExecutorService threadPool = new ThreadPoolExecutor(
3, //코어 스레드 개수
100, //최대 스레드 개수
120L, //놀고 있는 시간
TimeUnit.SECONDS, //놀고 있는 시간 단위
new SyncronousQueue<Runnable>() //작업 큐
);
- EcecutorService 종료와 관련된 메소드
→ 남아있는 작업을 마무리하고 스레드풀을 종료할 땐 shutdown( ), 남아있는 작업과 상관 없이 강제 종료할 땐 shutdownNow( ) 호출
리턴 타입 | 메소드명 | 설명 |
void | shutdown( ) | 현재 처리 중인 작업뿐만 아니라 작업 큐에 대기하고 있는 모든 작업을 처리한 뒤에 스레드를 종료시킨다. |
List<Runnable> | shutdownNow( ) | 현재 작업 처리 중인 스레드를 interrupt해서 작업 중지를 시도하고 스레드풀을 종료시킨다. 리턴값은 작업 큐에 있는 미처리된 작업(Runnable)의 목록이다. |
boolean | awaitTermination(long timeout, TimeUnit unit) | shutdown( ) 메소드 호출 이후, 모든 작업 처리를 timeout 시간 내에 완료하면 true를, 완료하지 못하면 작업 처리 중인 스레드를 interrupt하고 false를 리턴한다. |
- 하나의 작업은 Runnable 또는 Callable 구현 클래스로 표현한다.
1) Runnable의 run( ) 메소드는 리턴값이 없다.
2) Callable의 call( ) 메소드는 리턴값이 있다.
→ implements Callable<T>에서 지정한 T 타입의 리턴값
//Runnable
Runnable task = new Runnable() {
@Override
public void run() {
//스레드가 처리할 작업 내용
}
}
//Callable
Callable<T> task = new Callable<T>() {
@Override
public T call() throws Exception {
//스레드가 처리할 작업 내용
return T;
}
}
- 작업 처리 요청: ExecutorService의 작업 큐에 Runnable 또는 Callable 객체를 넣는 행위
→ execute( )는 작업 처리 도중 예외 발생 시 스레드가 종료되고 해당 스레드는 스레드풀에서 제거되므로 스레드풀은 다른 작업 처리를 위해 새로운 스레드를 생성하는 반면, submit( )은 작업 처리 도중 예외가 발생해도 스레드가 종료되지 않고 다음 작업을 위해 재사용된다.
리턴 타입 | 메소드명 | 설명 |
void | execute(Runnable command) | - Runnable을 작업 큐에 저장 - 작업 처리 결과를 받지 못함 |
Future<?> Future<V> Future<V> |
submit(Runnable task) submit(Runnable task, V result) submit(Callable<V> task) |
- Runnable 또는 Callable을 작업 큐에 저장 - 리턴된 Future를 통해 작업 처리 결과를 얻을 수 있음 |
- Future 객체는 작업 결과가 아니라 작업 완료를 기다렸다가(블로킹되었다가) 최종 결과를 얻는데 사용된다.
1) Future의 get( ) 메소드를 호출하면 스레드가 작업을 완료할 때까지 블로킹되었다가 작업을 완료하면 처리 결과를 리턴한다.
2) 작업을 처리하는 스레드가 작업을 완료하기 전까지 get( ) 메소드가 블로킹되므로 다른 코드를 실행할 수 없다.
→ get( ) 메소드를 호출하는 스레드는 새로운 스레드이거나 스레드풀의 또 다른 스레드가 되어야 한다.
리턴 타입 | 메소드명 | 설명 |
void | get( ) | 작업이 완료될 때까지 블로킹되었다가 처리 결과 V를 리턴 |
V | get(long timeout, TimeUnit unit) | timeout 시간 전에 작업이 완료되면 결과 V를, 작업이 완료되지 않으면 TimeoutException을 발생시킴 |
- 리턴값이 없는 작업일 경우 Runnable 객체로 생성하면 된다.
- 결과값이 없는 작업 처리 요청은 submit(Runnable task) 메소드를 이용하면 된다.
→ Future 객체를 리턴: 스레드가 작업 처리를 정상적으로 완료했는지 여부를 확인하기 위해 (예외 처리 필요)
Future future = executorService.submit(task);
try {
future.get();
} catch (InterruptedException e) {
//작업 처리 도중 스레드가 interrupt 될 경우 실행할 코드
} catch (ExecutionException e) {
//작업 처리 도중 예외가 발생된 경우 실행할 코드
}
package ch12;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class NoResultExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
System.out.println("[작업 처리 요청]");
Runnable runnable = new Runnable() {
@Override
public void run() {
int sum = 0;
for (int i = 1; i <= 10; i++) {
sum += i;
}
System.out.println("[처리 결과] " + sum);
}
};
Future future = executorService.submit(runnable);
try {
future.get();
System.out.println("[작업 처리 완료]");
} catch (Exception e) {
System.out.println("[실행 예외 발생함]" + e.getMessage());
}
executorService.shutdown();
}
}
- 애플리케이션이 처리 결과를 얻어야 할 땐 Callable로 작업 객체를 생성하면 된다.
→ 제네릭 타입 파라미터 T는 call( ) 메소드가 리턴하는 타입이 되도록 한다.
Future<T> future = executirService.submit(task)
try {
T result = future.get();
} catch (InterruptedException e) {
//작업 처리 도중 스레드가 interrupt 될 경우 실행할 코드
} catch (ExecutionException e) {
//작업 처리 도중 예외가 발생된 경우 실행할 코드
}
package ch12;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class ResultByCallableExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
System.out.println("[작업 처리 요청]");
Callable<Integer> task = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 10; i++) {
sum += i;
}
return sum;
}
};
Future<Integer> future = executorService.submit(task);
try {
int sum = future.get();
System.out.println("[처리 결과] " + sum);
System.out.println("[작업 처리 완료]");
} catch (Exception e) {
System.out.println("[실행 예외 발생함]" + e.getMessage());
}
executorService.shutdown();
}
}
- 작업 처리 결과를 외부 객체에 저장: ExecutorService의 submit(Runnable task, V result) 메소드를 사용
1) V가 Result 타입이다.
2) 메소드 호출 즉시 Future<V>가 리턴되는데, Future의 get( ) 메소드를 호출하면 작업 완료 이후 v 타입 객체를 리턴한다.
3) 리턴된 객체는 submit( )의 두 번째 매개값으로 준 객체와 동일하다.
→ 스레드 처리 결과가 내부에 저장되어 있다는 차이점이 있다.
4) 작업 객체는 Runnable 구현 클래스로 생성하는데, 생성자를 통해 Result 객체를 주입받도록 해야 한다.
package ch12;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
class Result {
int accumValue;
synchronized void addValue(int value) {
accumValue += value;
}
}
public class ResultByRunnableExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
System.out.println("작업 처리 요청");
class Task implements Runnable {
Result result;
public Task(Result result) {
this.result = result;
}
@Override
public void run() {
int sum = 0;
for (int i = 1; i <= 10; i++) {
sum += i;
}
result.addValue(sum);
}
}
Result result = new Result();
Runnable task1 = new Task(result); //runnable 구현 객체를 매개값으로 대입
Runnable task2 = new Task(result);
Future<Result> future1 = executorService.submit(task1, result);
Future<Result> future2 = executorService.submit(task2, result);
try {
result = future1.get();
result = future2.get();
System.out.println("처리 결과 " + result.accumValue);
System.out.println("작업 처리 완료");
} catch (Exception e) {
e.printStackTrace();
System.out.println("실행 예외 발생함 " + e.getMessage());
}
executorService.shutdown();
}
}
- 작업 요청 순서대로 작업 처리가 완료되는 것은 아니다.
- CompletionService: 스레드풀에서 작업 처리가 완료된 것만 통보받을 때 사용
1) CompletionService 구현 클래스는 ExecutorCompletionService<V>이다.
2) poll( )과 take( ) 메소드를 이용해 처리 완료된 작업의 Future를 얻으려면 CompletionService의 submit( ) 메소드로 작업 처리 요청을 해야 한다.
리턴 타입 | 메소드명 | 설명 |
Future<V> | poll( ) | 완료된 작업의 Future를 가져옴. 완료된 작업이 없다면 null을 리턴. |
Future<V> | poll(long timeout, TimeUnit unit) | 완료된 작업의 Future를 가져옴. 완료된 작업이 없다면 timeout까지 블로킹됨. |
Future<V> | take( ) | 완료된 작업의 Future를 가져옴. 완료된 작업이 없다면 있을 때까지 블로킹됨. |
Future<V> | submit(Callable<V> task) | 스레드풀에 Callable 작업 처리 요청 |
Future<V> | submit(Runnable task, V result) | 스레드풀에 Runnable 작업 처리 요청 |
package ch12;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class CompletionServiceExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
CompletionService<Integer> completionService = new ExecutorCompletionService<Integer>(executorService);
System.out.println("작업 처리 요청");
for (int i = 0; i < 3; i++) {
completionService.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 10; i++) {
sum += i;
}
return sum;
}
});
}
System.out.println("처리 완료된 작업 확인");
executorService.submit(new Runnable() {
@Override
public void run() {
while (true) {
try {
Future<Integer> future = completionService.take();
int value = future.get();
System.out.println("처리 결과 " + value);
} catch (Exception e) {
break;
}
}
}
});
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
}
executorService.shutdownNow();
}
}
- 콜백(callback): 애플리케이션이 스레드에게 작업 처리를 요청한 후 스레드가 작업을 완료하면 특정 메소드를 실행하는 것
1) 콜백 메소드: 자동 실행되는 메소드
2) 콜백 방식은 작업 처리를 요청한 후 결과를 기다릴 필요 없이 다른 기능을 수행할 수 있다.
3) Runnable 구현 클래스를 작성할 때 콜백 기능을 구현할 수 있다.
→ java.nio.channels.CompletionHandler를 이용
4) CompletionHandler에는 작업을 정상 처리 완료했을 때 호출되는 completed( )와 작업 처리 도중 예외가 발생했을 때 호출되는 Failed( ) 메소드가 있다.
5) CompletionHandler의 V 타입 파라미터는 결과값의 타입, A는 첨부값의 타입이다.
→ 첨부값: 콜백 메소드에 결과값 이외에 추가적으로 전달되는 객체(필요 없다면 Void로 지정)
package ch12;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CallbackExample {
private ExecutorService executorService;
public CallbackExample() {
executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
}
private CompletionHandler<Integer, Void> callback = new CompletionHandler<Integer, Void>() {
@Override
public void completed(Integer result, Void atachment) {
System.out.println("completed() 실행: " + result);
}
@Override
public void failed(Throwable exc, Void attachment) {
System.out.println("failed() 실행: " + exc.toString());
}
};
public void doWork(final String x, final String y) {
Runnable task = new Runnable() {
@Override
public void run() {
try {
int intX = Integer.parseInt(x);
int intY = Integer.parseInt(y);
int result = intX + intY;
callback.completed(result, null);
} catch (NumberFormatException e) {
callback.failed(e, null);
}
}
};
executorService.submit(task);
}
public void finish() {
executorService.shutdown();
}
public static void main(String[] args) {
CallbackExample example = new CallbackExample();
example.doWork("3", "3");
example.doWork("3", "삼");
example.finish();
}
}
'Java > 이것이 자바다' 카테고리의 다른 글
제네릭 (0) | 2023.04.25 |
---|---|
이자바 12장(멀티 스레드) 확인문제 (0) | 2023.04.25 |
이자바 11장(기본 API 클래스) 확인문제 (0) | 2023.04.15 |
기본 API 클래스 (3) (0) | 2023.04.15 |
기본 API 클래스 (2) (0) | 2023.04.14 |
댓글