2023.02.21-2 Java복습
14. 멀티스레드
14.1 멀티스레드 개념
운영체제는 실행중인 프로그램을 프로세스로 관리한다.
멀티태스킹은 두 가지 이상의 작업을 동시에 처리하는 것을 말한다.
이때 운영체제는 멀티프로세스를 생성해서 처리한다. 하지만 멀티태스킹이 꼭 멀티프로세스를 뜻하지는 않는다.
하나의 프로세스 내에서 멀티 태스킹을 할 수 있도록 만들어진 프로그램들도 있다.
예를들어 메신저는 채팅작업을 하면서 동시에 파일 전송작업을 수행하기도한다.
하나의 프로세스가 두 가지 이상의 작업을 처리할 수 있는 이유는 멀티스레드가 있기 때문이다.
스레드는 코드의 실행 흐름을 말하는데 프로세스 내에 스레드가 두개라면 두개의 코드 실행흐름이 생긴다.
멀티프로세스가 프로그램 단위의 멀티태스킹이라면 멀티스레드는 프로그램 내부에서의 멀티태스킹이라고 볼 수 있다.
멀티 프로세스들은 서로 독립적이므로 하나의 프로세스에서 오류가 발생되도 다른 프로세스에 영향을 미치지 않는다.
하지만 멀티스레드는 프로세스 내부에서 생성되기 때문에 하나의 스레드가 예외를 발생시키면 프로세스가 종료되므로 다른 스레드에게 영향을 미친다.
멀티스레드는 데이터를 분할해서 병렬로 처리하는 곳에서 사용하기도 하고 안드로이드 앱에서 네트워크 통신을 하기위해 사용하기도 한다.
다수의 클라이언트 요청을 처리하는 서버를 개발할때도 사용된다.
프로그램 개발에잇어 멀티스레드는 꼭 필요한 기능이기 때문에 반드시 이해하고 활용해야한다.
14.2 메인 스레드
모든 자바 프로그램은 메인 스레드가 main()메소드를 실행하면서 시작된다.
메인 스레드 main()의 첫 코드부터 순차적으로 실행하고 main()메소드의 마지막 코드를 실행하거나 return문을 만나면 실행을 종료한다.
메인스레드는 필요에 따라 추가 작업 스레드를 만들어서 실행시킬 수 잇다.
싱글스레드에서는 메인 스레드가 종료되면 프로세스가 종료된다.
하지만 멀티스레드에서는 실행중인 스레드가 하나라도 있다면 프로세스는 종료되지 않는다.
메인스레드가 작업스레드보다 먼저 종료되더라도 작업스레드가 계속실행중이면 프로세스는 종료되지 않는다.
14.3 작업스레드 생성과 실행
멀티 스레드로 실행하는 프로그램을 개발하려면 먼저 몇개의 작업을 병렬로 실행할지 결정하고 각 작업별로 스레드를 생성해야한다.
자바 프로그램은 메인 스레드가 반드시 존재하기 때문에 메인 작업이외의 추가적인 작업 수만큼 스레드를 생성하면된다.
자바는 작업스레드로 객체로 관리하므로 클래스가 필요하다. Thread클래스로 직접 객체를 생성해도되지만 하위 클래스를 만들어 생성할 수도 있다.
14.3.1 Thread 클래스로 직접 생성
java.lang 패키지에 있는 Thread 클래스로부터 작업 스레드 객체를 직접 실행하려면 runnalbe 구현객체를 매개값으로 갖는 생성자를 호출하면된다.
Runnable은 스레드가 작업을 실행할 때 사용하는 인터페이스이다. Runnalbe에는 run() 메소드가 정의되어 있다.
구현 클래스는 run()을 재정의해서 스레드가 실행할 코드를 가지고 있어야 한다.
이 구현클래스는 작업 내용을 정의한 것으로 스레드에게 전달해야한다.
명시적으로 작성하지 않고 익명 구현객체를 매개값으로 사용할 수 있다. 오히려 이방법이 더 많이 사용된다.
스레드 객체를 생성했다고 해서 바로 작업 스레드가 실행되는게 아니라 start()메소드를 호출해야 시작할 수 있다.
public class BeepPrintExample {
public static void main(String[] args) {
//메인스레드 실행
//작업 스레드에 Runnable 구현객체 주입
Thread thread = new Thread(new 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) {e.printStackTrace();}
}
}
});
//작업 스레드 실행시작(위코드)
thread.start();
for (int i = 0; i < 5; i++) {
System.out.println("띵");
try {Thread.sleep(500);
} catch (Exception e) {e.printStackTrace();}
}
}
}14.3.2 Thread자식클래스로 생성
작업 스레드 객체를 생성하는 또 다른 방법은 Tread의 자식객체로 만드는 것이다.
Thread클래스를 상속한다음 run()메소드를 재정의해서 스레드가 실행할 코드를 잓어하고 객체르 생성하면된다.
실행방법은 동일하다.
명시적인 자식 클래스를 정의하지 않고 Thread 익명자식객체를 사용할 수도 있다.
public class BeepPrintExample {
public static void main(String[] args) {
// 메인스레드 실행시작
// 작업 스레드에 자식객체 생성
Thread thread = new Thread() {
@Override
public void run() {
// Toolkit객체 얻기
Toolkit toolkit = Toolkit.getDefaultToolkit();
for (int i = 0; i < 5; i++) {
toolkit.beep();
try {Thread.sleep(500);
} catch (Exception e) {e.printStackTrace();}
}
}
};
// 작업 스레드 실행시작(위코드)
thread.start();
for (int i = 0; i < 5; i++) {
System.out.println("띵");
try {Thread.sleep(500);
} catch (Exception e) {e.printStackTrace();}
}
}
}구현객체와 상속클래스를 따로 만들어서 사용하면 코드의 재사용성이 높아진다.
환경에따라서 사용하면 될 것 같다.
결론적으로 Runnable을 구현한 객체를 넣거나 Thread 클래스를 상속한 자식객체를 넣거나이다.
차이점이 무엇일까? 작동하는 원리는 같다.
자바에서는 다중 상속을 하용하지 않기 때문에, Thread 클래스를 확장하는 클래스는 다른 클래스를 상속받을 수 없다.
반면에 Runnable 인터페이스를 구현했을 경우에는 다른 인터페이스를 구현할 수 있을 뿐만 아니라, 다른 클래스도 상속받을 수 있다.
따라서 해당 클래스의 확장성이 중요한 상황이라면 Runnable 인터페이스를 구현하는 것이 더 바람직하다.
한가지 방법만 알면 다른사람이 작성한 스레드 코드가 다른 방법으로 되어있다면 해석이 어려울수잇다.
runnalbe로 생성해서 스레드 생성자에 대입해 만드는거나 스레드 클래스를 상속해서 만드는 것이나 동일하다.
어떤 것이 더 효율적이다라고 할 수 없지만 위의 차이점을 가진다고 할 수 있다.
14.4 스레드 이름
스레드는 자신의 이름을 가지고 있다.
메인 스레드는 main이라는 이름을 가지고 있고 작업스레드는 자동적으로 붙여진다.
다른 이름으로 설정하고 싶다면 Thread 클래스의 setName() 메소드를 사용하면된다.
스레드 이름은 디버깅할때 어떤 스레드가 작업을 하는지 조사할 목적으로 주로 사용된다.
현재 코드를 어떤 스레드가 실행하고 있는지 확인하려면 정적메소드인 currentThread()로 스레드 객체의 참조를 얻은다음
getName()메소드로 이름을 출력해보면 된다.
이름은 스레드가 시작하기전에 변경해줘야 반영된다.
public class ThreadNameExample {
public static void main(String[] args) {
// 메인스레드 객체 얻기
Thread mainThread = Thread.currentThread();
System.out.println(mainThread.getName() + " 실행");
for (int i = 0; i < 3; i++) {
Thread threadA = new Thread() {
@Override
public void run() {
System.out.println(getName() + " 실행");
}
};
threadA.start();
}
Thread chatThread = new Thread() {
@Override
public void run() {
System.out.println(getName() + " 실행");
}
};
//이름변경
chatThread.setName("chat-thread");
chatThread.start();
}
}main 실행
Thread-0 실행
Thread-1 실행
Thread-2 실행
chat-thread 실행
14.5 스레드 상태
스레드 객체를 생성(NEW)하고 start()메소드를 호출하면 곧바로 스레드가 실행되는게 아니라 실행 대기상태 RUNNABLE상태가 된다.
실행대기상태른 실행을 기다리고 있는 상태를 말한다.
실행대기하는 스레드는 CPU 스케쥴링에 따라 CPU를 점유하고 run()메소드를 실행한다.
이때를 실행(RUNNING)상태라고 한다. 실행스레드는 run()메소드를 모두 실행하기전에 스케쥴링에 의해 다시 실행대기 상태로 돌아갈 수 있다.
그리고 다른 스레드가 실행상태가 된다.
스레드는 이런 식으로 실행 대기 상태와 실행상태를 번갈아가면서 자신의 run()메소드를 조금씩 실행한다.
실행 상태에서 run()메소드가 종료되면 더이상 실행할 코드가 없기때문에 스레드 실행은 멈췌되고 종료상태 TERMINATED라고 한다.
실행상태에서 일시정지상태로 가기도 하는데 일시 정지 상태는 스레드가 실행할 수 없는 상태를 말한다.
스레드가 다시 실행상태로 가기 위해서는 일시 정지 상태에서 실행 대기 상태로 가야만한다.

wait() notify() notify All()은 Object클래스의 메소드이고 그 외는 Thread크래스의 메소드이다.
14.5.1 주어진 시간 동안 일시정지 sleep()
실행 중인 스레드를 일정 시가나 멈추게하고 싶다면 Thread 클래스의 정적메소드인 sleep()을 이용하면된다.
매개값으로는 얼마 동안 일시 정지 상태로 있을 것인지 밀리세컨드(1/1000)단위로 시간을 주면된다.
일시정지상태에서는 예외가 발생할 수있기 때문에 예외처리를 해줘야한다.
다음 에제는 3초주기로 비프음 10번을 발생시킨다.
public class SleepExample {
public static void main(String[] args) {
Toolkit toolkit = Toolkit.getDefaultToolkit();
for (int i = 0; i < 10; i++) {
toolkit.beep();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}14.5.2 다른 스레드의 종료를 기다림
스레드는 다른 스레드와 독립적으로 실행하지만 다른 스레드가 종료될때 까지 기다렷다가 실행을 해야하는 경우도 있다.
예를들어 계산스레드의 작업이 종료된 후 그 결과값을 받아 처리하는 경우이다.
이를 이해 스레드는 join()메소드를 제공한다.
ThreadA가 ThreadB의 join()메소드를 호출하면 ThreadA는 ThreadB가 종료될때까지 일시정지 상태가 된다.
ThreadB의 run()메소드가 종료되어야 ThreadA다음 코드를 실행한다.
public class SumThread extends Thread {
private long sum;
public long getSum() {
return sum;
}
public void setSum(long sum) {
this.sum = sum;
}
@Override
public void run() {
/*
for (int i = 1; i <= 100; i++) {
sum += i;
}
*/
//sum = n *(시작항 + 마지막항)/2
sum = 100 * (100+1) / 2;
}
}public class JoinExample {
public static void main(String[] args) {
SumThread sumThread = new SumThread();
sumThread.start();
System.out.println("1~100합:" + sumThread.getSum()); // 0
try {
sumThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("1~100합:" + sumThread.getSum()); // 5050
}
}14.5.3 다른 스레드에게 실행양보 yield()
스레드가 처리하는 작업은 반복적인 실행을 위해 for문이나 while문을 포함하는 경우가 많다.
무의미한 반복을 하는 경우가 있다.
이때는 다른 스레드에게 실행을 양보하고 자신은 실행 대기 상태로 가는 것이 프로그램 성능에 도움이 된다.
Thread의 yield()메소드를 사용하면된다.
public class WorkThread extends Thread {
//필드
public boolean work = true;
//생성자
public WorkThread(String name) {
setName(name);
}
//메소드
@Override
public void run() {
while (true) {
if (work) {
System.out.println(getName() + ": 작업처리");
} else {
Thread.yield();
}
}
}
}public class YieldExample {
public static void main(String[] args) {
WorkThread wokrThreadA = new WorkThread("wokrThreadA");
WorkThread wokrThreadB = new WorkThread("wokrThreadB");
wokrThreadA.start();
wokrThreadB.start();
try {Thread.sleep(5000);
} catch (InterruptedException e) {e.printStackTrace();}
wokrThreadA.work = false;
try {Thread.sleep(10000);
} catch (InterruptedException e) {e.printStackTrace();}
wokrThreadA.work = true;
}
}14.6 스레드 동기화
멀티스레드는 하나의 객체를 공유해서 작업할 수도 있다.
이 경우 다른 스레드에 의해 객체 내부데이터가 쉽게 변경될 수 있기 때문에 의도햇던것과는 다른 결과가 나올수도있다.
스레드가 사용중인 객체를 다른스레드가 변경할 수 없도록 하려면 스레드 작업이 끝날때까지 객체에 잠금을 걸면된다.
이를 위해 자바는 동기화 메소드와 블록을 제공한다.
객체 내부에 동기화 메소드와 동기화 블록이 여러개가 있다면 스레드가 이들 중 하나를 실행할대
다른 스레드는 해당메소드는 물론이고 다른 동기화 메소드 및 블록도 실행할 수 없다.
일반 메소드는 실행 가능하다.
14.6.1 동기화 메소드 및 블록 선언
동기화 메소드를 선언하는 방법은 synchronized 키워드를 붙이면된다.
synchronized키워드는 인스턴스와 정적 메소드 어디든 붙일 수 있다.
스레드가 동기화 메소드를 실행하는 즉시 객체는 잠금이 일어나고 메소드 실행이 끝나면 잠금이 풀린다.
메소드 전체가 아닌 일부 영역을 실행할때만 객체잠금을 걸고 싶다면 동기화 블록을 만들면된다.
public class Calculator {
private int memory;
public int getMemory() {
return memory;
}
public synchronized void setMemory1(int memory) {
this.memory = memory;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": " + this.memory);
}
public void setMemory2(int memory) {
synchronized(this) {
this.memory = memory;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": " + this.memory);
}
}
}public class User1Thread extends Thread{
private Calculator calculator;
public User1Thread() {
setName("User1Thread");
}
public void setCalculator(Calculator calculator) {
this.calculator = calculator;
}
@Override
public void run() {
//동기화 메소드 호출
calculator.setMemory1(100);
}
}public class User2Thread extends Thread{
private Calculator calculator;
public User2Thread() {
setName("User2Thread");
}
public void setCalculator(Calculator calculator) {
this.calculator = calculator;
}
@Override
public void run() {
//동기화 메소드 호출
calculator.setMemory2(50);
}
}public class SynchronizedExample {
public static void main(String[] args) {
Calculator calculator = new Calculator();
User1Thread user1Thread = new User1Thread();
user1Thread.setCalculator(calculator);
user1Thread.start(); //100
User2Thread user2Thread = new User2Thread();
user2Thread.setCalculator(calculator);
user2Thread.start(); //50
}
}User1Thread이 setMemory1()을 실행하는 순간 Calculator객체를 잠근다.
따라서 User2Thread는 객체가 잠금해제 될때까지 Calculator의 동기화 블록을 실행하지 못한다.
14.6.2 wait()와 notify()를 이용한 스레드 제어
경우에 따라서는 두 개의 스레드를 교대로 번갈아 가며 실행할 때도 있다.
정확한 교대 작업은 필요할 경우 자신의 작업이 끝나면 상대방 스레드를 일시 정지 상태에서 풀어주고 자신은 일시정지 상태로 만들면 된다.
이 방법의 핵심은 공유객체에 있다. 공유 객체는 두 스레드가 작업할 내용을 각각 동기화 메소드로 정해 놓는다.
한 스레드가 작업을 완료하면 notify() 메소드를 호출해서 일시정지에 있는 다른 스레드를 실행대기상태로 만들고 자신은 wait()메소드를 호출하여 일시정지상태로 만든다.
notify()는 wait()에 의해 일시 정지된 스레드 중 한개를 실행 대기상태로 만들고 notifyAll()은 wait()에 의해 일시정지된 모든 스레드를 실행 대기상태로 만든다.
주의할점은 두 메소드는 동기화 메소드 또는 동기화 블록내에서만 사용할 수 있다.

public class WorkObject {
public synchronized void methodA() {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + ": methodA 작업 실행");
notify(); //다른 스레드를 실행 대기 상태로 만듦
try {
wait(); //자기를 일시 정지로
} catch (InterruptedException e) {}
}
public synchronized void methodB() {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + ": methodB 작업 실행");
notify(); //다른 스레드를 실행 대기 상태로 만듦
try {
wait(); //자기를 일시 정지로
} catch (InterruptedException e) {}
}
}public class ThreadA extends Thread{
private WorkObject workObject;
public ThreadA(WorkObject workObject) {
setName("ThreadA");
this.workObject = workObject;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
workObject.methodA();
}
}
}public class ThreadB extends Thread{
private WorkObject workObject;
public ThreadB(WorkObject workObject) {
setName("ThreadB");
this.workObject = workObject;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
workObject.methodB();
}
}
}public class WaitNotifyExample {
public static void main(String[] args) {
//공유할 작업객체
WorkObject workObject = new WorkObject();
ThreadA threadA = new ThreadA(workObject);
ThreadB threadB = new ThreadB(workObject);
threadA.start();
threadB.start();
}
}출력1 - 상대부르기 - 본인대기 - 출력2 - 상대부르기 - 본인대기 순서대로 반복됨
14.7 스레드 안전 종료
스레드는 자신의 run()메소드가 모두 실행되면 자동적으로 종료되지만 경우에 따라서는 실행중인 스레드를 즉시 종료할 필요가 있다.
예를들어 동영상을 보다가 사용자가 멈춤을 요구하는 경우이다.
스레드를 강제 종료하기 위해 stop()메소드가 있지만 갑자기 종료하면 리소스들이 불안전한 상태로 남게 되어 사용하지 않는다.
스레드를 안전하게 종료하는 방법은 interrupt()메소드를 이용하는게 좋다.
14.7.1 조건이용
스레드가 while문으로 반복 실행할 경우 조건을 이용해서 run()메소드의 종료를 유도할 수 있다.
public class PrintThread extends Thread {
private boolean stop;
public void setStop(boolean stop) {
this.stop = stop;
}
@Override
public void run() {
while (!stop) {
System.out.println("실행 중");
}
System.out.println("리소스 정리");
System.out.println("실행 종료");
}
}public class SafeStopExample {
public static void main(String[] args) {
PrintThread printThread = new PrintThread();
printThread.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {}
printThread.setStop(true);
}
}14.7.2 interrupt()메소드 이용
interrupt()메소드는 스레드가 일시정지 상태에 있을때 InterruptedException예외를 발생시키는 역할을 한다.
이것을 이용하면 예외처리를 통해 run()메소드를 정상종료시킬 수 있다.
public class PrintThread extends Thread {
@Override
public void run() {
try {
while (true) {
System.out.println("실행 중");
Thread.sleep(1);
}
} catch (InterruptedException e) {
}
System.out.println("리소스 정리");
System.out.println("실행 종료");
}
}public class InterruptExample {
public static void main(String[] args) {
Thread thread = new PrintThread();
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
thread.interrupt();
}
}PrintThreadd는 0.001초마다 휴식하면서 계속실행 main은 실행후 1초간 일시정지
일시정지 후에 메인스레드가 thread.interrupt();를 실행
그러면 휴식중일때 InterruptedException 발생
예외발생햇으니 while문빠져나옴 리소스 정리, 실행 종료
스레드가 실행 대기/ 실행상태일때는 interruput()메소드가 호출되어도 InterruptedException이 발생하지 않는다.
스레드가 어떤 이유로 일시정지 상태가 되면 InterruptedException이 발생한다.
그래서 짧은 시간이나마 일시정지를 위해 Thread.sleep(1);을 사용했다.
일시 정지를 만들지 않고도 interruput()메소드 호출 여부를 알 수 있는 방법이 잇다.
Thread의 interrupted()와 isInterrupted()메소드는 interruput()메소드의 호출 여부를 리턴한다.
interrupted()는 정적메소드이고 isInterrupted()는 인스턴스메소드이다.
억지로 재우면 비효율적이기 때문에 Thread.sleep(1);대신 interrupted()를 이용해서 확인한다음 빠져나가도록 해보자.
public class PrintThread extends Thread {
@Override
public void run() {
while (true) {
System.out.println("실행 중");
if (Thread.interrupted()) {
break;
}
}
System.out.println("리소스 정리");
System.out.println("실행 종료");
}
}public class InterruptExample {
public static void main(String[] args) {
Thread thread = new PrintThread();
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
thread.interrupt();
}
}14.8 데몬 스레드
데몬(daemon)스레드는 주 스레드의 작업을 돕는 보조적인 역할을 수행하는 스레드이다.
주 스레드가 종료되면 데몬 스레드도 따라서 자동으로 종료된다.
데몬스레드를 적용한 예로는 워드의 자동저장 JVM의 가비지컬렉터 등이있다.
주 스레드가 종료되면 데몬 스레드도 같이 종료된다.
스레드를 데몬으로 만들기 위해서는 주 스레드가 데몬이 될 스레드의 setDaemon(true)를 호출하면된다.
public 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("메인 스레드 종료");
}
}메인스레드 3초간 일시정지후 일어나서 종료를 출력하게함. 메인스레드가 종료되면 데몬스레드도 같이 종료된다.
14.9 스레드 풀
병렬작업 처리가 많아지면 스레드의 개수가 폭증하여 CPU가 바빠지고 메모리 사용량이 늘어난다.
이에따라 애플리케이션의 성능또한 급격히 저하된다.
이렇게 병렬작업 증가로 인한 스레드폭증을 막으려면 스레드풀을 사용하는 것이 좋다.
스레드풀은 작업 처리에 사용되는 스레드를 제한된 개수만큼 정해주고 작업 큐에 들어오는 작업들을 스레드가 하나씩 맡아 처리하는 방식이다.
작업처리가 끝난 스레드는 다시 작업큐에서 새로운 작업을 가져와 처리한다.
이렇게 하면 작업량이 증가해도 스레드개수가 늘어나지 않아 성능이 저하되지 않는다.

14.9.1 스레드풀 생성
자바는 스레드풀을 생성하고 사용할 수 있도록 java.util.concurrent 패키지에서 ExecutorService인터페이스와 Executors클래스를 제공하고 있다.
Executors의 정적 메소드를 사용하면 스레드풀인 ExecutorService구현객체를 만들 수 있다.
메소드명(매개변수) | 초기수 | 코어수 | 최대수
newChacedThreadPool() | 0 | 0 | Integer.MAX_VALUE
newFixedThreadPool(int nThreads)| 0 | 생성된수 | nThreads
초기수는 스레드풀이 생성될때 기본적으로 생성되는 스레드 수를 말한다.
코어수는 스레드가 증가된후 사용되지않는 스레드를 제거할때 최소한 풀에서 유지하는 스레드 수이다.
최대 수는 증가되는 스레드의 한도 수 이다.
newChacedThreadPool() 메소드로 생성된 스레드풀의 초기 수와 코어 수는 0개이고
작업 개수가 많아지면 새 스레드를 생성시켜 작업을 처리한다.
60초동안 스레드가 아무 작업을 하지 않으면 스레드를 풀에서제거한다.
newFixedThreadPool()로 생성된 스레드풀의 초기 수는 0개이고
작업개수가 많아지면 최대 nThreads까지 생성시켜 작업을 처리한다.
이 스레드풀의 특징은 생성된 스레드를 제거하지 않는다는 것이다.
ThreadPoolExecutor로 스레드풀을 직접 생성할 수도 잇다.
1.코어스레드개수 2.최대스레드개수 3.놀고있는시간 4.놀고있는시간단위 5.작업큐이다.
14.9.2 스레드풀 종료
스레드풀의 스레드는 기본적으로 데몬스레드가 아니기때문에 main 스레드가 종료되더라도 작업을 처리하기 위해 계속 실행상태로 남아있는다.
스레드풀의 모든 스레드를 종료하려면 ExecutorService의 메소들르 사용해야한다.
void shutdown() | 현재 처리 중인 작업뿐만 아니라 작업큐에 대기하고 있는 모든 작업을 처리한 후 스레드풀을 종료시킨다.
List<Runnable> shoutdownNow() | 현재 작업 처리 중인 스레드를 interrupt해서 작업을 중지시키고 스레드풀을 종료시킨다. 리턴값은 작업큐에 있는 미처리된 작업(Runnable객체)이다.
남은 작업을 마무리하고 스레드폼을 종료할때는 전자 남아있는 작업 상관없이 강제종료시엔 후자를 호출한다.
14.9.3 작업 생성과 처리 요청
하나의 작업은 Runnable 또는 Callable 구현객체로 표현한다.
차이점은 리턴값이 있느냐 없느냐 이다.
1.Runnable 익명구현객체
new Runnable(){
@Override
public void run(){
//스레드가 처리할 작업내용
}
}
2.Callable 익명구현객체
new Callable(){
@Override
public T call() throws Exception {
//스레드가 처리할 작업내용
return T;
}
}
Runnable의 run()메소드는 리턴값이 없고 Callable의 call()메소드는 리턴값이 있다.
call()의 리턴타입은 Callable<T>에서 지정한 T타입 파라미터와 동일한 타입이어야한다.
작업처리 요청이란
ExecutorService의 작업큐에 두 객체를 넣는 행위를 말한다.
작업처리요청을 위해서 다음의 메소드를 사용한다.
void execute(Runnable command) | Runnable을 작업큐에 저장 작업처리결과를 리턴하지 않음
Future<T> submit(Callable<T> task) | Callable을 작업 큐에 저장 작업처리결과를 얻을 수있도록 Future리턴
작업큐에 들어오면 ExecutorService는 처리할 스레드가 있는지를 보고 없다면 스레드를 새로 생성시킨다.
스레드는 작업 큐에서 각 객체를 꺼내와 run() 또는 call()메소드를 실행하면서 작업을 처리한다.
14.9.4 Runnable 사용
public class RunnableExecuteExample {
public static void main(String[] args) {
// 1000개의 메일 생성
String[][] mails = new String[1000][3];
for (int i = 0; i < mails.length; i++) {
mails[i][0] = "admin@my.com";
mails[i][1] = "member" + i + "@my.com";
mails[i][2] = "신상품 입고";
}
// ExecutorService 생성
ExecutorService executorService = Executors.newFixedThreadPool(5);
// 이메일 보내는 작업 생성 및 처리 요청
for (int i = 0; i < 1000; i++) {
// 익명구현객체에서 i값이 final이 되기때문에 설정해서 값을 직접넣어준다
final int idx = i;
executorService.execute(new Runnable() {
@Override
public void run() {
Thread thread = Thread.currentThread();
String from = mails[idx][0];
String to = mails[idx][1];
String content = mails[idx][2];
System.out.printf("[%s] %s ==> %s: %s\n", thread.getName(), from, to, content);
}
});
}
executorService.shutdown();
}
}14.9.4 Callable 사용
자연수를 덧셈하는 작업
public class CallableSubmitExample {
public static void main(String[] args) throws InterruptedException, ExecutionException {
// ExecutorService 생성
ExecutorService executorService = Executors.newFixedThreadPool(5);
// 계산작업 생성 및 처리 요청
for (int i = 1; i <= 100; i++) {
final int idx = i;
Future<Integer> future = executorService.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int j = 1; j <= idx; j++) {
sum += j;
}
Thread thread = Thread.currentThread();
System.out.printf("[%s] 1~%d 합 계산", thread.getName(), idx);
return sum;
}
});
int result = future.get();
System.out.println("\t리턴값: " + result);
}
//스레드풀 종료
executorService.shutdown();
}
}2023.02.21 후기
스레드의 개념과 사용방법은 잘 이해되는 것 같다. 실제로 사용은 스레드풀 정도만 사용해봐서 실제 실무에 적용되었을 경우를 찾아볼 필요가 있다.