Jun's Development Journey

[Java/문법] 스레드-1 본문

JAVA/문법

[Java/문법] 스레드-1

J_Jayce 2022. 1. 4. 16:09

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