스레드란?
CPU가 독립적으로 처리하는 하나의 작업 단위
프로세스를 이루는 작은 단위
멀티 스레드
- 어플리케이션 내부에서의 멀티 태스킹
- 멀티 스레드가 모여 프로세스를 이루고 프로세스가 모여 멀티 프로세스를 이룸
메인 스레드
- main() 메소드의 첫 코드부터 순차적으로 실행
- 필요에 따라 작업 스레드들을 만들어 병렬로 코드 실행 가능(=멀티 스레드)
싱글 스레드 어플리케이션 vs 멀티 스레드 어플리케이션
- 싱글 스레드 어플리케이션
: 메인 스레드가 종료되면 프로세스도 종료
- 멀티 스레드 어플리케이션
: 실행 중인 작업 스레드가 남아있으면 종료되지 않음
작업 스레드
- 생성
1. Thread 클래스로부터 직접 생성(Runnable로 생성)
Thread thread = new Thread(new Runnable() { // 혹은 Runnable 인터페이스를 구현한 객체 대입
@Override
public void run() {
// 코드 내용
}
});
// 혹은
Thread thread = new Thread(() -> {
// 코드 내용
});
2. Runnable로 만들지 않고 Thread의 하위 클래스로부터 생성
Thread thread = new Thread() { // 혹은 Thread 클래스를 상속한 객체 대입
@Override
public void run() {
// 코드 내용
}
};
- 실행
thread.start();
- 스레드 이름
thread.setName(설정할 이름); // 스레드 이름 설정
thread.getName(); // 스레드 이름 가져오기
- 스레드 실행 상태
thread.getState();
// State.NEW, State.TERMINATED, State.RUNNABLE, State.BLOCKED, State.WAITING 등 반환
- 현재 실행하는 스레드 얻기
Thread thread = Thread.currentThread();
스레드 스케줄링
: 동시성, 병렬성
스레드 스케줄링에 의해 스레드들은 아주 짧은 시간을 번갈아가면서 각자의 run() 메소드를 실행
우선순위 방식과 라운드 로빈 방식을 함께 사용
- 우선순위 방식
: 1~10, default=5
setPriority() 메소드를 이용하여 우선순위 변경
// 우선순위 지정 예시
thread.setPriority(Thread.MAX_PRIORITY); // 10
thread.setPriority(Thread.NORM_PRIORITY); .. 5
thread.setPriority(Thread.MIN_PRIORITY); // 1
- 라운드 로빈 방식
: 시간 할당량(Time Slice)을 정해서 그 시간만큼 각자 실행
우선순위가 같은 스레드의 병렬 실행에서 사용
공유 객체 사용 시 주의할 점
: 멀티 스레드 프로그램에서 스레드들이 객체를 공유하여 작업하는 경우, 중간에 어떤 스레드에 의해 상태가 변경된다면 다른 스레드들이 잘못된 값을 참조하는 경우 발생
→ 사용 중인 객체는 다른 스레드가 변경할 수 없도록 임계 영역을 선언해야 함
- 임계 영역
: synchronized를 붙여서 선언
// 1. 동기화 메소드 선언
public synchronized void method() { // 한 스레드가 동기화 메소드 사용시 다른 메소드는 종료될 때까지 사용 불가
...
// 2. 동기화 블록 선언
synchronized() {
...
}
...
}
- 동기화 메소드와 동기화 블록이 여러 개 있을 경우, 한 스레드가 그 중 하나만 실행중이어도 다른 스레드는 모든 동기화 메소드와 블록에 접근할 수 없음
스레드 객체
1. 생성
: 스레드 객체가 생성되고 start() 메소드가 호출되지 않은 상태
2. 대기
: start() 메소드 호출 후 자기 차례가 올 때까지 대기
3. 실행
: 스레드 스케줄링으로 선택된 스레드가 CPU를 점유하고 run() 메소드 실행
4. 일시정지
: 스레드가 실행될 수 없는 상태, 대기↔실행 구간에서 발생
5. 종료
: 코드 실행이 완료되어 종료
스레드 상태 제어
: 일시 정지, 종료
메소드 | 설명 |
interrupt() | 일시 정지 상태의 스레드에서 인터럽트 예외를 발생시킴 catch에서 실행 대기 상태로 가거나 종료 상태로 갈 수 있게 하는 메소드 |
notify() notifyAll() |
동기화 블록 내에서 wait() 메소드에 의해 일시 정지 상태가 된 스레드를 실행 대기 상태로 변환 |
sleep(long mills) sleep(long mills, int nanos) |
주어진 시간 동안 호출한 스레드(자기 자신)를 일시 정지 해당 시간 이후에는 실행 대기 상태가 됨 |
join() join(long mills) join(long mills, int nanos) |
호출한 스레드가 종료될 때까지 다른 모든 스레드를 일시 정지 1. 매개값이 주어진 경우, 해당 시간이 지나면 자동적으로 실행 대기 상태가 됨 2. 매개값이 주어지지 않은 경우, 해당 스레드가 종료괴면 실행 대기 상태가 됨 |
wait() wait(long mills) wait(long mills, int nanos) |
동기화 블록 내에서 스레드를 일시 정지 1. 매개값이 주어진 경우, 해당 시간이 지나면 자동적으로 실행 대기 상태가 됨 2. 매개값이 주어지지 않은 경우, notify()나 notifyAll() 메소드를 실행하여 실행 대기 상태가 됨 |
yield() | 우선순위가 동일한 다른 스레드에게 실행을 양보하고 실행 대기 |
stop() | 스레드 즉시 종료 |
1. sleep()
: 주어진 시간동안 일시정지
2. yield()
: 다른 스레드에게 실행 양보
3. join()
: 다른 스레드의 종료를 기다림
4. wait() ↔ notify(), notifyAll()
: 스레드 간 협업(두 개 이상의 스레드 실행 시)
스레드 종료
: run() 메소드가 모두 실행되면 자동으로 스레드 종료
→ 스레드를 즉시 종료하려면 플래그 변수를 이용하거나 interrupt() 메소드를 이용하여 예외처리를 하는 방법이 있음
// 스레드 인터럽트 발생 예시
public void run() {
try {
while(true) {
sout("실행중");
Thread.sleep(1); // interrupt()를 사용하기 위해서는 스레드가 일시정지 상태여야 하므로 sleep(1) 사용
}
} catch(InterruptedException e) { ... }
}
- 인터럽트 메소드 호출 여부
boolean status = Thread.interrupted(); // 현재 스레드 기준
// 혹은
boolean status = objThread.isInterrupted(); // 지정한 스레드 기준
데몬(daemon) 스레드
: 주 스레드의 작업을 돕는 보조 스레드, 주 스레드가 종료되면 같이 종료
스레드를 데몬으로 만들기 위해서는 데몬이 될 스레드의 setDaemon(true) 메소드를 호출하면 됨
main() {
...
thread.setDaemon(true); // start() 메소드가 실행되기 전에 setDaemon()이 실행되어야 함
thread.start();
}
스레드 그룹
: 관련된 스레드를 묶어서 관리할 목적으로 이용
- 장점: 그룹 내에 포함된 모든 스레드들을 일괄 interrupt 가능
- 스레드 그룹 이름 얻기
ThreadGroup group = Thread.currentThread.getThreadGroup();
String groupName = group.getName();
- 모든 스레드에 대한 정보 얻기
Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces(); // Map 타입으로 리턴Set<Thread> Set<Thread> threads = map.keySet(); // 모든 스레드의 목록을 Set 타입으로 리턴
for(Thread t : threads) { // 기타 스레드 정보 얻기
System.out.println("이름: " + t.getName() + (t.isDaemon() ? "(데몬)" : "(주)"));
System.out.println("소속그룹: " + t.getThreadGroup().getName());
}
- 스레드 그룹 생성
ThreadGroup tg = new ThreadGroup(그룹이름);
// 혹은
ThreadGroup tg = new ThreadGroup(부모 스레드 그룹, 그룹이름);
- 생성된 스레드 그룹 사용
Thread t = new Thread(ThreadCroup group, Runnable target);
Thread t = new Thread(ThreadCroup group, Runnable target, String name);
Thread t = new Thread(ThreadCroup group, Runnable target, String name, long stackSize);
Thread t = new Thread(ThreadCroup group, String name);
- 스레드 그룹 메소드
메소드 | 설명 | |
int | activeCount() | 현재 그룹 및 하위 그룹에서 활동 중인 모든 스레드의 갯수 리턴 |
int | activeGroupCount() | 현재 그룹에서 활동 중인 하위 그룹의 수 리턴 |
void | checkAccess() | 현재 스레드가 스레드 그룹을 |
void | destroy() | 현재 그룹 및 하위 그룹 삭제 |
boolean | isDestroyed() | 현재 그룹이 삭제되었는지 여부 리턴 |
int | getMaxPriority() | 현재 그룹에 포함된 스레드의 최대 우선순위 리턴 |
void | setMaxPriority() | 현재 그룹에 포함된 스레드의 최대 우선순위 설정 |
String | getName() | 현재 그룹의 이름 리턴 |
ThreadGroup | getParent() | 현재 그룹의 부모 그룹 리턴 |
boolean | parentOf(ThreadGroup g) | 현재 그룹이 매개값으로 지정된 스레드 그룹의 부모인지 여부 리턴 |
boolean | isDaemon() | 현재 그룹이 데몬 그룹인지 리턴 |
void | setDaemon() | 현재 그룹을 데몬 그룹으로 설정 |
void | list() | 현재 그룹에 포함된 스레드와 하위 그룹에 대한 정보 출력 |
void | interrupt() | 현재 그룹에 포함된 모든 스레드 interrupt |
스레드풀(ThreadPool)
: 작업 처리에 사용되는 스레드를 제한된 갯수만큼 정해놓고 작업 큐에서 들어오는 작업들을 하나씩 스레드가 맡아 처리
어플리케이션의 성능 저하 예방
ExecutorService, Executors 클래스를 이용하여 생성
- 스레드풀의 동작 방식
1. 어플리케이션이 스레드풀에 작업 처리 요청
2. 각 스레드가 작업 큐에서 작업을 가져와 처리
3. 어플리케이션에 결과 전달
- 스레드풀 생성
: newCachedThreadPool() 혹은 newFixedThreadPool(스레드 개수) 메소드로 생성
// 1. newCachedThreadPool로 생성
ExecutorService executorService = Executors.newCachedThreadPool();
// 2. newFixedThreadPool로 생성
ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
// 3. ThreadPoolExecutor로 생성 → 직접 스레드 갯수와 타임 리밋 설정 가능
ExecutorService threadPool = new ThreadPoolExecutor(
3, 100, 120L, Timeunit.SECONDS, new SynchronousQueue<Runnable>()
// 코어 스레드, 최대 스레드, 놀고 있는 시간, 놀고 있는 시간 단위
);
1. newCachedThreadPool()
: 초기 스레드 개수와 코어 스레드 개수는 0개
스레드 개수보다 작업 개수가 많으면 새 스레드 생성
스레드가 60초 동안 아무 작업도 하지 않으면 종료
2. newFixedThreadPool(n)
: 스레드 개수보다 작업 개수가 많으면 새 스레드 생성
스레드가 아무 작업을 하지 않더라도 종료하지 않음
- 스레드풀 종료
: main 스레드가 종료되도 스레드풀은 실행 상태로 남아있으므로 어플리케이션을 완전히 종료하기 위해서는 따로 스레드풀을 종료해야 함
executorService.shutdown(); // 작업 큐에 있는 모든 작업들이 완료되면 종료
// 혹은
executorService.shutdownNow(); // 진행중인 작업을 중단하고 종료
작업
- 작업 생성: Runnable 또는 Callable 인터페이스로 구현된 클래스 사용
// Runnable 구현 클래스 예시
Runnable task = new Runnable() {
@Override
public void run() {
...
}
}
// Callable 구현 클래스
Callable<T> task = new Callable<T> {
@Override
public T call() throws Exception {
...
return T;
}
}
: Runnable은 리턴값 X, Callable은 T를 리턴
→ 작업 큐에 Runnable 또는 Callable 객체를 넣어서 작업 요청
- 작업 처리 요청 메소드
1. execute()
: Runnable 객체만 사용 가능, void
작업 처리중 예외가 발생하면 스레드 종료 및 스레드 풀에서 제거 → 새로운 스레드 생성해서 사용
2. submit() → 권장
: Runnable, Callable 객체 사용 가능, 작업 처리 결과를 포함한 객체(Future) 리턴
작업 처리중 예외가 발생해도 스레드는 종료되지 않고 재사용 → 새로운 스레드 생성 X
Future 객체(=지연 완료 객체)
: submit() 메소드 사용시 반환되는 결과 객체
// 작업 생성
ExecutorService executorService = Executors.newFixedThreadPool( Runtime.getRuntime().availableProcessors());
// 작업 실행 및 결과 객체 저장
Future<Integer> future = executorService.submit(task);
// 결과 객체 사용
try {
int result = future.get();
sout("결과 = " + result);
}
- 사용되는 작업 클래스별 리턴 타입
1. submit(Runanble task)
→ null 반환(리턴값이 없으므로)
try {
future.get();
} catch(InterruptedException e) {
// 작업 처리 도중 스레드가 인터럽트 될 경우 실행
} catch(ExecuteException e) {
// 작업 처리 도중 예외가 발생할 경우 실행
}
2. submit(Runanble task, Integer result)
→ int 타입 값 반환
3. submit(Callable<String> task)
→ String 타입 값 반환
- 메소드 목록
리턴 타입 | 메소드명 | 설명 |
V | get() get(long timeout, TimeUnit unit) |
작업이 완료될 때까지 기다렸다가 처리 결과 V 리턴(블로킹) timeout 변수 사용시 해당 시간 전에 작업이 완료되면 결과 V 리턴, 완료되지 않으면 TimeoutException 발생 |
boolean | cancel(bool 변수) | 진행중인 작업 취소 |
boolean | isCancelled() | 작업이 취소되었는지 여부 |
boolean | isDone() | 작업이 완료되었는지 여부 |
CompletionService
: 스레드풀에서 작업 처리가 완료된 작업을 가져올 수 있는 클래스
작업 완료 순으로 통보될 수 있도록 poll(), take() 메소드 제공
// completionService 생성
ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
CompletionService<Integer> completionService = new ExecutorCompletionService<Integer>(executorService);
- 메소드 목록
리턴 타입 | 메소드명 | 설명 |
Future<V> | poll() poll(long timeout, TimeUnit unit) |
완료된 작업의 Future를 가져옴 완료된 작업이 없으면 즉시 null 리턴 |
Future<V> | take() | 완료된 작업의 Future를 가져옴 완료된 작업이 없으면 작업이 완료될 때까지 대기 |
Future<V> | submit(Callable<V> task) submit(Runnable task, V result) |
스레드풀에 작업 처리 요청 |
→ 처리 완료된 작업의 Future를 얻으려면 completionService.submit(); 메소드로 작업 처리 요청을 해야 함
- 예시
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.shutdown();
}
}
}
작업 완료 통보
: 콜백 방식과 블로킹 방식이 있음
- 블로킹 방식
: 작업 처리를 요청한 후 작업이 완료될 때까지 블로킹, 콜백 방식은 작업 처리를 요청한 후 결과를 기다릴 필요 없이 다른 기능 수행 가능
- 콜백 방식
: 스레드가 작업을 완료하면 특정 메소드를 자동 실행하는 기법
Runnable 구현 클래스를 작성할 때 CompletionHandler를 사용하여 구현 가능
private CompletionHandler<Integer, Void> callback = new CompletionHandler<Integer, Void>() {
@Override public void completed(Integer result, Void attachment) {
... // 작업을 정상적으로 처리했을 때 호출
}
@Override public void failed(Throwable exc, Void attachment) {
... // 작업중 예외가 발생했을 경우 호출
}
};
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);
callback.completed(result, null); // 작업을 정상적으로 처리했을 때 호출
} catch (NumberFormatException e) {
callback.failed(e, null); // 작업중 예외가 발생했을 경우 호출
}
}
};
executorService.submit(task);}
'Language > Java' 카테고리의 다른 글
Java - 람다식 (0) | 2021.07.18 |
---|---|
Java - 제네릭 (0) | 2021.07.07 |
Java - 기본 API 클래스 정리 (0) | 2021.06.25 |
Java - 중첩 클래스와 중첩 인터페이스, 예외 처리 복습 (0) | 2021.06.25 |
Java - 인터페이스 복습 (0) | 2021.06.24 |