Jun's Development Journey
[Java/문법] 스레드-1 본문
1. 메인 스레드
- 모든 자바 어플리케이션은 메인 스레드가 main() 메소드를 실행하면서 시작된다.
- main() 메소드의 첫 코드부터 순차적으로 실행하고, 마지막 코드를 실행하거나 return문을 만나면 실행이 종료된다.ss
2. 스레드 우선순위
- 멀티 스레드는 동시성과 병렬성으로 실행되기 때문에 이 용어들에 대해 정확히 이해하는 것이 중요하다.
- 동시성은 멀티 작업을 위해 하나의 코어에서 멀티 스레드가 번갈아가며 실행하는 성질이다.
- 병렬성은 멀티 작업을 위해 멀티 코어에서 개별 스레드를 동시에 실행하는 성질이다.
- 싱글 코어 CPU를 이용한 멀티 스레드 작업은 병렬적으로 실행되는 것처럼 보이지만, 사실은 번갈아가며 실행하는 동시성 작업이다. 번갈아 실행하는 것이 워낙 빠르다보니 병렬성으로 보이는 것이다.
- 스레드 스케쥴링에 의해 스레드들은 아주 짧은 시간에 번갈아가면서 그들의 run() 메소드를 조금씩 실행한다.
- 자바의 스레드 스케쥴링은 우선순위 방식과 라운드 로빈 방식을 사용한다.
- 우선순위 방식은 1 ~ 10까지 부여되는데, 1이 가장 우선순위가 낮고, 10이 가장 높다. 우선순위를 변경하려면 setPriority() 메소드를 이용하면 된다.
ex) Thread 우선순위 예시
- 10번째 스레드에만 제일 높은 우선순위를 주고 나머지는 제일 낮은 우선순위를 주었더니 20억번 루핑을 10번이 가장 먼저 끝내고 나머지는 라운드 로빈으로 실행되었다.
3. 동기화 메소드와 동기화 블록
- 멀티 스레드 프로그램에서는 스레드들이 객체를 공유해서 작업해야 하는 경우가 있다. 이 경우 스레드 A를 사용하던 객체가 스레드 B에 의해 상태가 변경될 수 있기 때문에 A가 의도했던 것과는 다른 결과가 산출될 수도 있다.
- 위의 경우 예시 코드(공유 객체에 대해 동기화가 안되있어서 한번 50으로 바뀐 것을 다른 스레드가 그래도 가져간다)
3-1) 동기화 메소드 및 동기화 블록
- 스레드가 사용중인 객체를 다른 스레드가 변경할 수 없도록 하려면 스레드 작업이 끝날 때까지 객체에 잠금을 걸어서 다른 스레드가 사용할 수 없도록 해야한다.
- 멀리 스레드 프로그램에서 단 하나의 스레드만 실행할 수 있는 코드 영역을 임계 영역(Critical Section)이라고 한다.
- 자바에서는 임계 영역을 지정하기 위해 동기화(synchronized) 메소드와 동기화 블록을 제공한다.
- 스레드가 객체 내부의 동기화 메소드 또는 블록에 들어가면 즉시 객체에 잠금을 걸어 다른 스레드가 임계영역 코드를 실행하지 못하도록 한다. 스레드가 동기화 메소드를 실행 종료하면 잠금이 풀린다.
- 메소드 전체 내용이 아니라 일부 내용만 임계 영역으로 만들고 싶다면 동기화 블록을 사용하면 된다.
- 동기화 블록 예시
public void method(){
//여러 스레드가 실행 가능 영역
......
synchronized(공유객체){ // 공유객체가 객체 자신이면 this를 넣을 수 있다.
임계 영역 // 단 하나의 스레드만 실행
}
}
- 위 코드 동기화 적용 예시
1) 동기화 메소드
2) 동기화 블록
4. 스레드 상태
- 스레드 객체를 생성하고, start() 메소드를 호출하면 곧바로 스래드가 실행되는 것처럼 보이지만 사실은 실행 대기 상태이다.
- 실행 대기 상태란 아직 스케쥴링이 되지 않아서 실행을 기다리고 있는 상태를 말한다. 실행 대기 상태에 있는 스레드 중에서 스케쥴링으로 선택된 스레드는 비로소 CPU를 점유하고 run() 메소드를 실행한다. 이때를 실행(Running) 상태라한다.
- 실행 상태 스레드는 스케쥴링에 의해 다시 실행 대기 상태로 갈 수 있고, 이때 실행 대기 상태이던 다른 스레드가 선택되어 실행 상태가 된다. 이렇게 실행 대기 상태, 실행 상태를 번갈아가면서 자신의 run() 메소드를 조금씩 실행한다.
- 실행 상태에서 run() 메소드가 종료되면, 더 이상 실행할 코드가 없기 때문에 종료 상태로 된다.
- 경우에 따라 일시 정지 상태로 가기도 한다. WAITING, TIMED_WAITING, BLOCKED 상태가 있다.
- 스레드 상태 예시
1) 스레드 상태 출력하는 StatePrintThread
- 생성자 매개값으로 받은 타겟 스레드의 상태를 0.5초 주기로 출력한다.
2) TargetThread 클래스
- 첫 번째 10억번 루핑돌며 runnable 상태를 유지
- sleep에 의해 1.5초간 TIMED_WAITING 상태를 유지
- 두 번째 10억번 루핑 돌며 runnable 상태를 유지
3) 실행 클래스
5. 스레드 상태 제어
- 멀티 스레드 프로그래밍이 어려운 이유 중 하나가 정교한 스레드 상태 제어에 있다.
- 스레드는 잘 사용하면 약이 되지만, 잘못 사용되면 치명적인 프로그램 버그가 될 수 있다.
- 스레드 제어를 잘 하기 위해서는 스레드의 상태 변화를 가져오는 메소드들을 파악하고 있어야 한다.
메소드 | 설명 |
interrupt() | 일시 정지 상태의 스레드에서 InterruptedException 예외를 발생시켜, 예외 처리 코드 (catch)에서 실행 대기 상태로 가거나 종료 상태로 갈 수 있도록 한다. |
notify() notifyAll() |
동기화 블록 내에서 wait() 메소드에 의해 일시 정지 상태에 있는 스레드를 실행 대기 상태로 만든다. |
sleep(long millis) sleep(long millis, int nanos) |
주어진 시간 동안 스레드를 일시 정지 상태로 만든다. 주어진 시간이 지나면 자동적으로 실행 대기 상태가 된다. |
join() join(long millis) join(long millis, int nanos) |
join() 메소드를 호출한 메소드는 일시 정지 상태가 된다. 실행 대기 상태로 가려면, join()메소드를 멤버로 가지는 스레드가 종료되거나 매개값으로 주어진 시간이 지나야한다. |
wait() wait(long millis) wait(long millis, int nanos) |
동기화 블록 내에서 스레드를 일시 정지 상태로 만든다. 매개값으로 주어진 시간이 지나면 자동적으로 실행 대기 상태가 된다. 시간이 주어지지 않으면 notify(), notifyAll() 메소드에 의해 실행 대기 상태로 갈 수 있다. |
yield() | 실행 중에 우선순위가 동일한 다른 스레드에게 실행을 양보하고 실행 대기 상태가 된다. |
5-1) yield()
- 스레드가 처리하는 작업은 반복적인 실행을 위해 for문이나 while문을 포함하는 경우가 많다. 가끔 이 반복문들은 무의미한 반복을 하는 경우가 있다.
- ex
- 만약 work 값이 false이고 true로 변경되는 시점이 불명확하다면 while문은 어떠한 실행문도 실행하지 않고 무의미한 반복을 한다.
- 이것보단 다른 스레드에게 실행을 양보하고 자신은 실행 대기 상태로 가는 것이 전체 프로그램에 성능을 높여준다.
- 다음 코드는 의미없는 반복을 줄이기 위해 yield() 메소드를 호출해서 다른 스레드에게 실행 기회를 주도록 수정한 것이다.
- ex
- 스레드 실행 양보 예제
- 처음 실행 후 3초 동안 ThreadA,B가 번갈아가며 실행된다.
- 3초 뒤 메인 스레드가 ThreadA의 work 필드를 false로 바꿔 A는 yield() 메소드를 호출한다.
- 이후에 3초동안엔 ThreadB가 더 많은 실행 기회를 얻는다.
- 메인 스레드는 3초 뒤 다시 ThreadA의 work 필드를 true로 변경하여, 번갈아가며 실행되도록 한다.
- 마지막 3초 뒤에 두 스레드 모두 stop 플레그를 변경하여 종료시킨다.
5-2) 다른 스레드의 종료를 기다림 join()
- 스레드는 다른 스레드와 독립적으로 실행하는 것이 기본적이지만 다른 스레드가 종료될 때까지 기다렸다가 실행해야 하는 경우가 발생할 수 있다.
- 예를 들어 계산 작업을 하는 스레드가 모든 계산 작업을 마쳤을 때 계산 결과값을 받아 이용하는 경우가 이에 해당한다.
- 메인 스레드는 SumThread가 계산 작업을 모두 마칠 때까지 일시 정지 상태에 있다가 SumThread가 최종 계산된 결과값을 산출하고 종료하면 결과값을 받아 출력한다.
- thead.join()을 통해서 메인 스레드를 일시 정지시킨다.
5-3) 스레드 간 협업(wait(), notify(), notifyAll()
- 두 개의 스레드를 교대로 번갈아가며 실행해야 할 경우가 있다. 정확한 교대 작업이 필요할 경우, 자신의 작업이 끝나면 상대방 스레드를 일시 정지 상태에서 풀어주고, 자신은 일시 정지 상태로 만드는 것이다.
- 이 방법에 핵심은 공유 객체에 있다. 공유 객체는 두 스레드가 작업할 내용을 각각 동기화 메소드로 구분해 놓는다. 한 스레드가 작업을 완료하면 notify() 메소드를 호출하여 일시 정지 상태에 있는 다른 스레드를 실행 대기 상태로 만들고, 자신은 두 번 작업을 하지 않도록 wait() 메소드를 호출하여 일시 정지 상태로 만든다.
-ex
5-4) 스레드의 안전한 종료
5-4-1) interrupt() 메소드 이용하는 방법
- ex
- 주목할 점은 스레드가 실행 대기 또는 실행 상태에 있을 때 interrup() 메소드가 실행되면 즉시 InterruptedException 예외가 발생하지 않고, 스레드가 미래에 일시 정지 상태가 되면 예외가 발생한다는 것이다.
- 따라서 스레드가 일시 정지 상태가 되지 않는다면 interrupt() 메소드 호출은 아무 의미가 없다. 그래서 짧은 시간이나마 일시정지 하기 위해 Thread.sleep(1)을 사용한 것이다.
- 일시 정지하지 않고도 interrupt() 호출 여부를 알 수 있는 방법이 있다.
boolean status1 = Thread.interrupted();
boolean status2 = objThread.interrupted();
=> 현재 스레드가 interrupt 되었는지 여부를 확인할 수 있는 메소드이다.
- ex
6. 데몬 스레드
- 주 스레드의 작업을 돕는 보조적인 역할을 수행하는 스레드이다.
- 예로는 워드 프로세서의 자동 저장, 미디어 플레이어의 동영상 및 음악 재생, 가비지 컬렉션 등이 있는데, 이 기능들은 주 스레드(워드 프로세서, 미디어 플레이어, JVM)가 종료되면 같이 종료된다.
- 스레드를 데몬으로 만들기 위해서는 주 스레드가 데몬이 될 스레드의 setDaemon(true)을 호출해주면 된다.
- 주의할 점은 start() 메소드가 호출되고 나서 setDaemon(true)를 호출하면 IllegalThreadStateException이 발생하기 때문에 start() 메소드 호출 전에 setDaemon(true)를 호출해야 한다.
- ex
'JAVA > 문법' 카테고리의 다른 글
[Java/문법] 인터페이스 (0) | 2022.01.14 |
---|---|
[Java/문법] 스레드 -2 (0) | 2022.01.06 |
[Java/문법] 입출력(I/O) (0) | 2022.01.04 |
[Java/문법] 클래스 (0) | 2022.01.03 |
[Java/문법] Main 메소드 (0) | 2022.01.03 |