일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
Tags
- 딥러닝
- OpenAI
- 해커톤
- 지도학습
- gpt
- Machine Learning
- LG Aimers 4th
- 오블완
- LLM
- GPT-4
- 회귀
- LG
- deep learning
- AI
- ChatGPT
- 머신러닝
- regression
- LG Aimers
- 티스토리챌린지
- PCA
- 분류
- supervised learning
- Classification
Archives
- Today
- Total
SYDev
[이것이 자바다] Chapter 14. 멀티 스레드 본문
- 데이터 분할 병렬 처리할 때
- 안드로이드 앱에서 네트워크 통신할 때
- 다수의 클라이언트 요청을 처리하는 서버를 개발할 때
1. 메인 스레드
- 모든 자바 프로그램은 main thread가 main() method를 실행하면서 시작
- main thread는 main() method의 첫 코드부터 순차적으로 실행
- main() method의 마지막 코드를 실행하거나, return 문을 만나면 실행 종료
- main thread는 필요에 따라 추가 작업 스레드들을 만들어 실행
- 실행중인 thread가 하나라도 있다면 process는 종료되지 않음
2. 작업 스레드 생성과 실행
2.1. Thread 클래스로 직접 생성
Thread thread = new Thread(Runnable target);
- Runnable: threaed가 task를 실행할 때 사용하는 interface
- run() method가 정의돼있음 -> 구현 클래스는 thread가 실행할 코드를 포함
class Task implements Runnable {
@Override
public void run() {
// thread가 실행할 코드
}
}
- 명시적인 Runnable 구현 클래스를 작성하지 않고, Thread 생성자를 호출할 때 Runnable 익명 구현 객체를 매개값으로 사용하는 방법을 더 많이 사용
Thread thread = new Thread( new Runnable() {
@Override
public void run() {
// thread가 실행할 코드
}
} );
- 작업 스레드 객체를 생성하고, 작업 스레드를 실행하려면 thread 객체의 start() method를 호출
thread.start();
- 예제
package ch14.sec3;
import java.awt.Toolkit;
public class BeepPrintExample {
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) {}
}
}
}
-> beep음을 모두 발생시키고 나서, "띵" 5번 출력
package ch14.sec3;
import java.awt.Toolkit;
public class BeepPrintExample {
public static void main(String[] args) {
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) {}
}
}
});
// 작업 thread 실행
thread.start();
for(int i = 0; i < 5; i++) {
System.out.println("띵");
try { Thread.sleep(500); } catch(Exception e) {}
}
}
}
-> main thread는 출력을 담당, 작업 thread에서는 동시에 beep음 처리
2.2. Thread 자식 클래스로 생성
- 작업 thread 객체를 생성하는 또 다른 방법 -> Thread의 자식 객체로 만드는 방법
- Thread 클래스를 상속한 다음 run() method를 재정의 -> thread가 실행할 코드를 작성하고 객체 생성
public class WorkerThread extends Thread {
@Override
public void run() {
// thread가 실행할 코드
}
}
// thread 객체 생성
Thread thread = new WorkerThread();
- Thread 익명 자식 객체를 사용하는 방법
Thread thread = new Threaed() {
@Override
public void run() {
// thread가 실행할 코드
}
};
thread.start();
- 예제
package ch14.sec3_1;
import java.awt.Toolkit;
public class BeepPrintExample {
public static void main(String[] args) {
Thread thread = new 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) {}
}
}
}
3. 스레드 이름
- thread는 자신의 이름을 가짐
- main thread는 'main'이라는 이름을 가짐
- worker thread는 자동적으로 'Thread-n'이라는 이름을 가짐
- worker thread 이름을 다른 이름으로 설정하고 싶다면, Thread 클래스의 setName() method 사용
thread.setName("스레드 이름");
- thread name은 주로 디버깅할 때, 어떤 thread가 작업을 하는지 조사할 목적으로 주로 사용
- 현재 코드를 어떤 thread가 실행하고 있는지 확인하려면 -> static method인 currentThread()로 thread 객체의 참조를 얻은 다음, getName() method name을 출력
Thread thread = Thread.currentThreaed();
System.out.println(threaed.getName());
4. 스레드 상태
- thread object를 생성(NEW)하고, start() method를 호출하면 바로 스레드가 실행되는 것이 아니라 실행 대기 상태(RUNNABLE)가 됨
- 실행(RUNNING) 상태: CPU scheduling에 따라 CPU를 점유하고 run() method를 실행
- running thread는 run() method를 모두 실행하기 전에 scheduling에 의해, 다시 실행 대기 상태로 돌아갈 수 있음
- 실행 상태에서 run() method 종료 -> 더이상 실행할 코드가 없기 때문에 threaed의 실행이 멈춤 -> 종료(TERMINATED) 상태
- 실행 상태에서 일시 정지 상태로 가기도 함 -> 일시 정지 상태는 thread가 실행할 수 없는 상태를 의미
- 스레드가 다시 실행 상태로 가기 위해서는 일시 정지 상태에서 실행 대기 상태로 가야함
4.1. 주어진 시간 동안 일시 정지
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
// interrupt() method가 호출되면 실행
}
- 예제
package ch14.sec5.exam1;
import java.awt.Toolkit;
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) {
}
}
}
}
-> 3초 주기로 beep음을 10번 발생
4.2. 다른 스레드의 종료를 기다림
- 다른 thread가 종료될 때까지 기다렸다가 실행해야 하는 경우
- 예제
package ch14.sec5.exam2;
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;
}
}
}
package ch14.sec5.exam2;
public class JoinExample {
public static void main(String[] args) {
SumThread sumThread = new SumThread();
sumThread.start();
try {
sumThread.join(); //sumThread 대기
} catch (InterruptedException e) {
}
System.out.println("1~100 합: " + sumThread.getSum());
}
}
- join으로 기다리지 않는 경우, 그냥 끝내버림
package ch14.sec5.exam2;
public class JoinExample {
public static void main(String[] args) {
SumThread sumThread = new SumThread();
sumThread.start();
// try {
// sumThread.join(); //sumThread 대기
// } catch (InterruptedException e) {
// }
System.out.println("1~100 합: " + sumThread.getSum());
}
}
4.3. 다른 스레드에게 실행 양보
- thread가 처리하는 작업은 무의미안 반복문을 실행하는 경우가 존재 -> 이때 다른 thread에게 실행을 양보하고 자신은 실행 대기 상태로 가는 것이 프로그램 성능에 도움
public void run() {
while(true) {
if(work) {
System.out.println("ThreadA 작업 내용");
}
}
}
-> 무의미한 반복
public void run() {
while(true) {
if(work) {
System.out.println("ThreadA 작업 내용");
} else {
Thread.yield();
}
}
}
- 예제
package ch14.sec5.exam3;
public class WorkThread extends Thread {
// field
public boolean work = true;
// constructor
public WorkThread(String name) {
setName(name);
}
// method
@Override
public void run() {
while(true) {
if(work) {
System.out.println(getName() + ": 작업처리");
} else {
Thread.yield();
}
}
}
}
package ch14.sec5.exam3;
public class YieldExample {
public static void main(String[] args) {
WorkThread workThreadA = new WorkThread("workThreadA");
WorkThread workThreadB = new WorkThread("workThreadB");
workThreadA.start();
workThreadB.start();
try { Thread.sleep(5000); } catch (InterruptedException e) {}
workThreadA.work = false;
try { Thread.sleep(10000); } catch (InterruptedException e) {}
workThreadA.work = true;
}
}
5. 스레드 동기화
- 멀티 스레드는 하나의 객체를 공유해서 작업할 수도 있음 -> 다른 thread에 의해 의도했던 것과 다른 결과 초래 가능성
- 이를 위해 자바는 synchronized method와 block을 제공
- 객체 내부에 synchronized method와 block이 여러 개 있으면, thread가 이들 중 하나를 실행할 때 다른 thread는 해당 method는 물론이고 다른 synchronized method 및 block도 실행 불가
- 일반 method는 실행 가능
5.1. 동기화 메소드 및 블록 선언
public synchronized void method() {
// 단 하나의 thread만 실행하는 영역
}
- 메소드 전체가 아닌 일부 영역을 실행할 때만 객체 잠금을 걸고 싶은 경우
public void method() {
// multiple threads가 실행 가능한 영역
synchronized(공유객체) {
// 단 하나의 thread만 실행 가능한 영역
}
// multiple threads가 실행 가능한 영역
}
- 예제
package ch14.sec6.exam1;
public class Calculator {
private int memory;
public int getMemory() {
return memory;
}
// synchronized method
public synchronized void setMemory1(int memory) {
this.memory = memory;
try {
Thread.sleep(2000);
} catch(InterruptedException e) {}
System.out.println(Thread.currentThread().getName() + ": " + this.memory);
}
// synchronized block
public void setMemory2(int memory) {
synchronized(this) {
this.memory = memory;
try {
Thread.sleep(2000);
} catch(InterruptedException e) {}
System.out.println(Thread.currentThread().getName() + ": " + this.memory);
}
}
}
package ch14.sec6.exam1;
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);
}
}
package ch14.sec6.exam1;
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);
}
}
package ch14.sec6.exam1;
public class SynchronizedExample {
public static void main(String[] args) {
Calculator calculator = new Calculator();
User1Thread user1Thread = new User1Thread();
user1Thread.setCalculator(calculator);
user1Thread.start();
User2Thread user2Thread = new User2Thread();
user2Thread.setCalculator(calculator);
user2Thread.start();
}
}
-> 동기화 블록 혹은 메소드 선언을 했기 때문에, 매개값을 입력한대로 예상한 결과 출력
-> setMemory에서 동기화 선언을 제거하고 실행한 경우
5.2. wait()과 notify()를 이용한 스레드 제어
- 두 개의 threads를 교대로 번갈하가며, 교대 작업이 필요할 경우
- 공유 객체 -> 두 threads가 작업할 내용을 각각 동기화 method로 설정
- 한 thread가 작업을 완료하면, notify() method 호출
- 일시 정지 상태의 thread -> 실행 대기 상태
- 자신은 두 번 작업을 하지 않도록 wait() method 호출
- 실행 -> 일시 정지 상태
- wait() & notify()는 synchronized method or synchronized block 내에서만 사용 가능
- 예제
package ch14.sec6.exam2;
public class WorkObject {
// synchronized method
public synchronized void methodA() {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + ": methodA 작업 실행");
notify(); // 다른 thread를 실행 대기 상태로 만
try {
wait(); // 자신의 thread를 일시 정지 상태로 만
} catch (InterruptedException e) {
}
}
public synchronized void methodB() {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + ": methodB 작업 실행");
notify();
try {
wait();
} catch (InterruptedException e) {
}
}
}
package ch14.sec6.exam2;
public class ThreadA extends Thread {
private WorkObject workObject;
// parameter로 공유 작업 객체를 받음
public ThreadA(WorkObject workObject) {
setName("ThreadA");
this.workObject = workObject;
}
@Override
public void run() {
for(int i = 0; i < 10; i++) {
workObject.methodA(); // synchronized method 호
}
}
}
package ch14.sec6.exam2;
public class ThreadB extends Thread {
private WorkObject workObject;
// parameter로 공유 작업 객체를 받음
public ThreadB(WorkObject workObject) {
setName("ThreadB");
this.workObject = workObject;
}
@Override
public void run() {
for(int i = 0; i < 10; i++) {
workObject.methodB(); // synchronized method 호
}
}
}
package ch14.sec6.exam2;
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();
}
}
6. 스레드 안전 종료
- thread는 run() method가 모두 실행되면 자동적으로 종료되지만, 경우에 따라 실행중인 thread를 즉시 종료할 필요가 있음
- ex) 동영상을 끝까지 보지 않고, 사용자가 멈춤을 요구하는 경우
- thread를 강제 종료시키기 위해 Thread는 stop() method를 제공 -> deprecated(더 이상 사용되지 않음)
- resources(file, network, ...)가 불안정한 상태로 남겨지기 때문
- thread를 안전하게 종료하는 방법: 사용하던 resources를 정리하고, run() method를 빨리 종료하는 것
- 조건 이용
- interrupt() method
6.1. 조건 이용
public class XXXThread extends Thread {
private boolean stop;
public void run() {
// stop이 true가 되면 반복문 탈출
while( !stop ) {
// thread가 반복 실행하는 코드;
}
// thread가 사용한 resource 정리
}
}
- 예제
package ch14.sec7.exam1;
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("실행 종료");
}
}
package ch14.sec7.exam1;
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);
}
}
-> main thread 3초 재웠다가, stop을 true로 설정
6.2. interrupt() 메소드 이용
- interrupt() method: 일시 정지 상태에 있을 때, InterruptedException 예외를 발생시키는 역할
- 예외 처리를 통해 run() method를 정상 종료 가능
- 예제
package ch14.sec7.exam2;
public class PrintThread extends Thread {
public void run() {
try {
while(true) {
System.out.println("실행 중");
Thread.sleep(1);
}
} catch(InterruptedException e) {
}
System.out.println("리소스 정리");
System.out.println("실행 종료");
}
}
package ch14.sec7.exam2;
public class InterruptExample {
public static void main(String[] args) {
Thread thread = new PrintThread();
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
thread.interrupt();
}
}
7. 데몬 스레드
- 데몬(Daemon) 스레드: main thread의 작업을 돕는 보조적인 역할을 수행하는 thread
- main thread가 종료되면, daemon thread도 따라서 자동으로 종료
- ex) 워드프로세서의 자동 저장, 미디어 플레이어의 동영상 및 음악 재생, 가비지 컬렉터 -> main thread(워드프로세스, 미디어 플레이어, JVM)가 종료되면 자동 종료
- thread를 daemon으로 만들기 위해, main thread가 daemon이 될 thread의 setDaemon(true)를 호출
public static void main(String[] args) {
AutoSaveThread thread = new AutoSaveThread();
thread.setDaemon(true);
thread.start();
...
}
- 예제
package ch14.sec8;
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();
}
}
}
package ch14.sec8;
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("메인 스레드 종료");
}
}
-> main thread 종료 시에, 자동으로 종료
8. 스레드풀
- 병렬 작업 처리가 많아지면 threads의 개수가 폭증 -> CPU가 바빠지고, memory 사용량이 늘어남 -> application 성능 저하
- 병렬 작업 증가로 인한 threads 폭증 방지 -> 스레드풀(ThreadPool) 사용
- ThreadPool: 작업 처리에 사용되는 threads를 제한된 개수만큼 정해놓고, Job Queue에 들어오는 작업들을 thread가 하나씩 맡아 처리하는 방식
8.1. 스레드풀 생성
- 자바는 ThreadPool을 생성하고 사용할 수 있도록, java.util.concurrent 패키지에서 ExecutorService interface와 Executors class를 제공
- 초기 수: 스레드풀이 생성될 때 기본적으로 생성되는 threads 수
- 코어 수: threads가 증가된 후 사용되지 않는 threads를 제거할 때, 최소한 풀에서 유지하는 threads 수
- 최대 수: 증가되는 threads의 한도 수
메소드명(매개변수) | 초기 수 | 코어 수 | 최대 수 |
newCachedThreadPool() | 0 | 0 | Integer.MAX_VALUE |
newFixedThreadPool(int nThreads) | 0 | 생성된 수 | nThreads |
- newCachedThreadPool() method
- 초기 수와 코어 수가 0
- 작업 개수가 많아지면 새 thread를 생성시켜 작업 처리
- 60초 동안 thread가 아무 작업을 하지 않으면 thread를 pool에서 제거
- newFixedThreadPool() method
- 초기 수 0
- 작업 개수가 많아지면 최대 nThreads개까지 thread를 생성시켜 작업을 처리
ExecutorService executorService = Executors.newCachedThreadPool();
ExecutorService executorService = Executors.newFixedThreadPool(5);
- method 사용 없이, 직접 ThreadPoolExecutor로 threadpool 생성도 가능
ExecutorService threadPool = new ThreadPoolExecutor(
3, // 코어 스레드 개수
100, // 최대 스레드 개수
120L, // 놀고 있는 시간
TimeUnit.SECONDS, // 놀고 있는 시간 단위
new SynchronousQueue<Runnable>() // 작업 큐
);
-> 120초 작업하지 않을 시에, 해당 thread를 pool에서 제거
8.2. 스레드풀 종료
- threadPool의 thread는 기본적으로 Daemon thread가 아니기 때문에, 자동 종료 X
- ExecutorService의 다음 두 메소드 중 하나를 실행해서 종료해야 함
리턴 타입 | 메소드명(매개변수) | 설명 |
void | shutdown() | 현재 처리 중인 작업뿐만 아니라 작업 큐에 대기하고 있던 모든 작업을 처리한 뒤에 threadPool을 종료시킴 |
List<Runnable> | shutdownNow() | 현재 작업 처리 중인 thread를 interrupt해서 작업을 중지시키고 threadPool을 종료시킴. 리턴값은 작업 큐에 있는 미처리된 작업(Runnable)의 목록 |
- shutdown(): 남아있는 작업을 마무리하고, threadPool을 종료
- shutdownNow(): 남아있는 작업과 상관없이 강제 종료
8.3. 작업 생성과 처리 요청
- 하나의 작업은 Runnable 또는 Callable 구현 객체로 표현됨
- 둘의 차이점은 작업 처리 완료 후 리턴값의 유무
new Runnable() {
@Override
public void run() {
// 스레드가 처리할 작업 내용
}
}
new Callable<T>() {
@Override
public T call() throws Exception {
// 스레드가 처리할 작업 내용
return T;
}
}
- 작업 처리 요청: ExecutorService의 job Queue에 Runnable 또는 Callable 객체를 넣는 행위
리턴 타입 | 메소드명(매개변수) | 설명 |
void | execute(Runnable command) | - Runnable을 job Queue에 저장 - 작업 처리 결과를 리턴 X |
Future<T> | submit(Callable<T> task) | - Callable을 job Queue에 저장 - 작업 처리 결과를 얻을 수 있도록 Future를 리턴 |
- 예제
package ch14.sec9.exam2;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
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++) {
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.println("[" + thread.getName() + "]" + from + " ==> " + to + ": " + content);
}
});
}
// ExecutorService 종료
executorService.shutdown();
}
}
-> 1000개의 Runnable 생성 후, execute() method로 job Queue에 입력
-> ExecutorService는 최대 5개의 threads로 job Queue에서 Runnable을 하나씩 꺼내어 run() method를 실행하며 작업 처리
참고자료
'Programming Lang > Java' 카테고리의 다른 글
[이것이 자바다] Chapter 16. 람다식 (2) | 2025.02.02 |
---|---|
[이것이 자바다] Chapter 15. 컬렉션 자료구조 (0) | 2025.02.02 |
[이것이 자바다] Chapter 13. 제네릭 (0) | 2025.01.31 |
[이것이 자바다] Chapter 12. java.base 모듈 (0) | 2025.01.31 |
[이것이 자바다] Chapter 11. 예외 처리 (0) | 2025.01.30 |