스레드
스레드 기본 정보
- 스레드 이름은 중복될 수 있지만 스레드 ID는 자바에서 직접 생성해주고 중복되지 않는다.
- 운영체제마다 다르지만 스레드의 우선순위가 높을수록 더 자주 실행된다.
- 1 ~ 10 까지 지정할 수 있으며 기본 값은 5로 지정된다.
- Thread 클래스의 toString()은 스레드 ID, 이름, 우선순위, 스레드 그룹을 포함해서 반환한다.
- 스레드 이름은 주로 로깅을 목적으로 지정한다.
- 이름을 지정해주지 않으면 자동으로 생성해서 지어준다.
ex) Thread-1, Thread-A
- 이름을 지정해주지 않으면 자동으로 생성해서 지어준다.
- 스레드는 그룹화해서 관리할 수 있으며 부모 스레드와 동일한 스레드 그룹에 속하게 된다.
- 그룹으로 특정 작업(일괄 종료, 우선순위 설정 등)을 수행할 수 있다.
- 스레드 그룹 기능은 직접적으로 잘 사용되지 않는 기능이다.
- 부모 스레드 : 새로운 스레드를 생성하는 스레드를 의미한다.
ex) 메인 스레드에서 threadA를 생성했다면 메인 스레드는 threadA의 부모 스레드다.
this란? : 어떤 메소드를 호출하는 것 = 특정 스레드가 어떤 메소드를 호출하는 것.
스레드는 메소드 호출을 관리하기 위해 메소드 단위로 스택 프레임을 만들어 스택 영역에 넣는다.
이때 인스턴스의 메소드를 호출하면 어떤 인스턴스의 메소드인지 구분하기 위해 해당 인스턴스의 참조 값을 스택 프레임 내부에 저장해두는데 그것이 this이다.
즉, this는 호출된 인스턴스 메소드가 소속된 객체를 가리키는 참조이며, 스택 프레임 내부에 저장되어 있다.
스레드 생명 주기
- 스레드의 상태
- New (새로운 상태): 스레드가 생성되었지만 시작되지 않은 상태
- Thread 객체가 생성 후 start() 메소드가 호출되지 않은 상태
ex) Thread thread = new Thread(runnable);
- Thread 객체가 생성 후 start() 메소드가 호출되지 않은 상태
- Runnable (실행 가능 상태): 스레드가 실행 중이거나 실행될 준비가 된 상태
- 실제로 CPU에서 실행될 수 있으며 start() 메소드가 호출된 상태
ex) thread.start() - Runnable 상태의 모든 스레드가 동시에 실행되는 것은 아니기 때문에 Runnable 상태의 스레드는 스케줄러의 실행 대기열에 포함되어 있다가 차례로 CPU에서 실행된다.
- 실제로 CPU에서 실행될 수 있으며 start() 메소드가 호출된 상태
- Blocked (차단 상태): 스레드가 동기화 락을 기다리는 상태
- synchronized 블록에 진입하기 위해 락을 얻어야 하는 경우의 상태이다.
- Waiting (대기 상태): 스레드가 무기한으로 다른 스레드의 작업을 기다리는 상태
- wait(), join() 메소드가 호출될 때의 상태로 다른 스레드가 notify(), notifyAll() 메소드를 호출하거나 join()이 완료될 때까지 기다린다.
- Timed Waiting (시간 제한 대기 상태): 스레드가 일정 시간동안 다른 스레드의 작업을 기다리는 상태
- sleep(millis), wait(timeout), join(millis) 메소드가 호출될 때의 상태로 주어진 시간이 경과하거나 다른 스레드가 해당 스레드를 깨울 때까지 기다린다.
- Terminated (종료 상태): 스레드의 실행이 완료된 상태
- 스레드가 정상적으로 종료되거나 예외가 발생하여 종료된 경우의 상태로 한 번 종료되면 다시 시작할 수 없다.
- New (새로운 상태): 스레드가 생성되었지만 시작되지 않은 상태
- 자바 스레드의 상태 전이 과정
- New -> Runnable : start() 메소드 호출 시
- Runnable -> Blocked / Waiting / Timed Waiting : 스레드가 락을 얻지 못하거나 wiat(), sleep() 메소드 호출 시
- Blocked / Waiting / Timed Waiting -> Runnable : 스레드가 락을 얻거나 기다림이 완료될 시
- Runnable -> Terminated : 스레드의 run() 메소드가 완료될 시
- sleep(millis)가 호출된 상태, 즉 Timed Waiting 상태의 스레드 상태를 확인하려면 자기 자신이 직접 확인하지 못하기에 다른 스레드가 확인해줘야 한다.
체크 예외 재정의
- Runnable 인터페이스에서 run()을 구현할 때 체크 예외를 밖으로 던질 수 없는 이유는?
- 자바에서는 부모 메소드가 체크 예외를 던지지 않으면, 재정의된 자식 메소드도 체크 예외를 던질 수 없다.
- Runnable 인터페이스의 run() 메소드 정의에서 체크 예외를 던지지 않기 때문에 run()을 구현하는 경우에도 체크 예외를 던질 수 없다.
- 자식 메소드는 부모 메소드가 던질 수 있는 체크 예외의 하위 타입만 던질 수 있다.
- 자식이 더 넓은 범위의 예외를 던지면 해당 코드에서 모든 예외를 제대로 처리하지 못하는 경우가 생길 수 있다.
- 언체크(런타임) 예외는 예외 처리를 강제하지 않기에 run() 구현 시 던질 수 있다.
- 그렇기에 런타임 예외를 제외한 체크 예외는 run() 구현 시 밖으로 던질 수 없고 run()에서 try-catch로 예외 처리를 강제함으로 스레드의 안정성과 일관성을 유지할 수 있다.
예외 처리가 되지 않아서 프로그램이 비정상적으로 종료되는 상황을 방지할 수 있다.
- 자바에서는 부모 메소드가 체크 예외를 던지지 않으면, 재정의된 자식 메소드도 체크 예외를 던질 수 없다.
join()
public class Join {
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;
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 스레드에서 처리하는 것이다.
하지만 다른 스레드들의 연산이 끝나기 전에 main 스레드가 먼저 종료되어 버린다. - 이 문제를 해결하기 위해서 다른 스레드들이 끝나기 전까지 기다려야하는데 그 역할을 하는 메소드가 sleep(), join()이다.
- sleep()을 사용하면 정확한 타이밍을 맞춰서 기다리기 어렵다.
- join()을 사용하면 특정 스레드가 종료될 때까지 해당 스레드를 대기시킬 수 있다.
- ex) main 스레드에서 thread1.join()을 호출하면 thread1 스레드가 종료될 때까지 대기한다.
- join() : join()을 호출하는 스레드는 WAITING 상태로 대상 스레드가 TERMINATED 상태가 될 때까지 대기한다. 이후 TERMINATED가 되면 호출 스레드는 다시 RUNNABLE 상태가 된다.
- 다른 스레드가 완료될 때까지 무기한 기다린다는 단점이 있다.
- join(ms): 호출 스레드는 특정 시간만큼만 대기하므로 무기한 기다리는 단점을 보완할 수 있다.
- 이때 호출한 스레드는 WAITING이 아닌 TIMED_WAITING 상태가 된다.
인터럽트
- 인터럽트 : 스레드를 종료, 일시 중단하는 기능으로 직접 강제 종료하는 대신 인터럽트를 통해 스레드에 요청을 보내 자발적으로 종료하도록 할 수 있는 기능이다.
- 주로 작업 취소 요청 / 대기 상태 종료 / 정상적인 스레드 종료를 위해 사용한다.
- interrupt() : 특정 스레드의 인스턴스에 interrupt() 메소드를 호출하면 해당 스레드에 인터럽트가 발생한다.
- interrupt()를 호출한 특정 스레드가 인터럽트 상태가 된다.
- 인터럽트 상태인 스레드를 sleep()과 같이 InterruptException을 발생하는 메소드를 호출하거나 호출 중일 때 InterruptException 예외가 발생한다.
- InterruptException 예외가 발생한 스레드는 인터럽트 상태에서 깨어나 Runnable 상태가 되며 코드를 정상 수행한다.
- 인터럽트를 통해 대기중인 스레드를 바로 깨워서 실행 가능한(정상적인) 상태로 바꿀 수 있다.
- 특정 flag 변수로 작업을 중단하는 경우에는 반응성이 느리기에 즉시 종료할 수 없다는 단점이 있지만 인터럽트를 활용하면 반응성이 빠르기에 좋다.
- 인터럽트 예외가 한 번 발생한 후 정상 상태로 되돌려주지 않는다면 이후에도 sleep()과 같은 코드를 호출 시 계속 인터럽트 예외가 발생하게 된다.
- 인터럽트의 목적을 달성하면 인터럽트 상태를 다시 정상으로 변경해둬야 한다.
그렇기에 InterrupException 예외가 발생한 후 스레드는 정상 상태로 변경된다.
- 인터럽트의 목적을 달성하면 인터럽트 상태를 다시 정상으로 변경해둬야 한다.
- 스레드 객체명.isInterrupt() : 스레드의 인터럽트 상태를 확인하는 용도로 상태를 바꾸지는 못한다.
- Thread.interrupted() : 스레드가 인터럽트 상태라면 상태를 반환하고 정상 상태로 변경하고, 정상 상태가 아니라면 상태를 반환하고 상태를 변경하지 않는다
- volatile : 여러 스레드가 동시에 접근하는 변수에 붙여줌으로 안전하게 할 수 있는 키워드이다.
- ConcurrentLinkedQueue : 여러 스레드가 동시에 접근하는 경우 동시성을 지원하는 동시성 컬렉션을 사용해야 하는데 그 중 동시성을 지원하는 Queue이다.
yield
public class YieldMain {
static final int THREAD_COUNT = 1000;
public static void main(String[] args) {
for (int i = 0; i < THREAD_COUNT; i++) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}
}
static class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + " - " + i); //1. empty
//sleep(1); //2.sleep
//Thread.yield(); //3.yield
}
}
}
}
- 1000개의 스레드를 1 ~ 10까지 출력하는 코드로 아무것도 사용하지 않은 경우와 sleep()을 사용한 경우, yield()를 사용한 경우로 나눠서 스레드가 수행하는 것을 보기 위한 코드이다.
- 아무것도 사용하지 않으면 운영체제의 스케줄링에 따라서 하나의 스레드가 실행되고 멈추는 것을 반복한다.
- 보통 한 개의 스레드가 연속으로 많은 작업을 실행한 후 다른 스레드로 전환된다.
- sleep()을 사용하면 스레드의 상태가 RUNNABLE -> TIMED_WAITING 상태로 변경되면서 스레드는 CPU 자원을 사용하지 않고 실행 스케줄링 큐에서 제외되었다가 시간이 지난 후 TIMED_WAITING -> RUNNABLE 상태로 변경되면서 실행 스케줄링 큐에 포함된다.
- 한 개의 스레드가 매우 적은 작업만 수행하고 다른 스레드로 전환된다.
- TIMED_WAITING 상태가 되면서 다른 스레드에 실행을 양보하고 해당 스레드는 특정 시간동안 스케쥴링 큐에서 제외되기 때문에 특정 시간만큼 스케줄링 큐에 대기 중인 다른 스레드가 CPU 실행 기회를 빨리 얻을 수 있다.
- 이 경우 과정이 복잡하고 특정 시간만큼 해당 스레드가 실행되지 않는다는 단점이 있다.
- 양보할 스레드가 없는 경우에도 해당 스레드마저 실행되지 못한다.
- yield()를 사용하면 RUNNABLE 상태를 유지하면서 현재 실행중인 스레드가 자발적으로 CPU를 양보하여 다른 스레드가 실행될 수 있도록한다.
- 아무것도 사용하지 않은 경우와 sleep()의 중간 정도로 sleep()을 사용한 경우보다는 한 스레드가 많이 작업을 수행하고 다른 스레드로 전환된다.
- yield()를 호출한 스레드는 RUNNABLE 상태를 유지하면서 CPU 사용 기회를 다른 스레드에게 양보하면서 다시 스케줄링 큐에서 대기하게 된다.
- yield()를 사용하더라도 반드시 양보하는 것도 아니고 양보할 대상이 없다면 해당 스레드가 계속 실행될 수도 있다.
출처 : [인프런 김영한 실전 자바 - 고급편]
김영한의 실전 자바 - 고급 1편, 멀티스레드와 동시성 강의 | 김영한 - 인프런
김영한 | 멀티스레드와 동시성을 기초부터 실무 레벨까지 깊이있게 학습합니다., 국내 개발 분야 누적 수강생 1위, 제대로 만든 김영한의 실전 자바[사진][임베딩 영상]단순히 자바 문법을 안다?
www.inflearn.com
'Java > [인프런 김영한 실전 자바 - 고급편]' 카테고리의 다른 글
[인프런 김영한 실전 자바 - 고급편] concurrent.Lock (0) | 2024.08.02 |
---|---|
[인프런 김영한 실전 자바 - 고급편] 동기화 - synchronized (0) | 2024.08.02 |
[인프런 김영한 실전 자바 - 고급편] 메모리 가시성 (0) | 2024.08.02 |
[인프런 김영한 실전 자바 - 고급편] 스레드 생성과 실행 (1) | 2024.07.30 |
[인프런 김영한 실전 자바 - 고급편] 프로세스와 스레드 소개 (2) | 2024.07.30 |