시작하기 전 ...
이전 포스팅에서는 스레드 라이프 사이클 - 상태에 대해서 다뤘다. 아래 링크 참고.
그럼 이번 포스팅에서는 상태를 어떻게 제어할 것인지에 대해 다룰 것이다.
Goal
- 스레드의 상태 제어란 무엇인지 알아본다.
- 주어진 시간 동안 일시 정지하는 sleep()
- 다른 스레드에게 실행 양보 yield()
- 다른 스레드의 종료를 기다리는 join()
- 스레드간 협업 wait(),notify(), notifyAll()
- 스레드의 안전한 종료 (stop 플래그, interrupt()
스레드 상태 제어란 ?
멀티 스레드 프로그램을 만들기 위해서는 정교한 스레드 상태 제어가 필요한데, 상태 제어가 잘못되면 프로그램은 불안정해져 먹통이 되거나 다운된다. 그렇기 때문에 스레드를 정확하게 제어하는 방법 - 스레드 상태 변화 메소드 인지 파악이 아주 중요하다.
가령,
사용자는 미디어 플레이어에서 동영상을 보다가 일시 정지를 하거나 종료할 수 있다.
- 일시정지의 경우 : 조금 후 다시 동영상을 보겠다는 의미로 미디어 플레이어는 동영상 스레드를 일시 정지 상태로 만들어야 한다.
- 종료의 경우 : 더 이상 동영상을 보지 않겠다는 의미로 미디어 플레이어는 스레드를 종료 상태로 만들어야 한다.
주어진 시간동안 일시 정지 sleep()
실행 중인 스레드를 일정 시간 멈추게 하고 싶다면 Thread 클래스의 정적 메소드인 sleep()을 사용하면 된다.
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){
}
}
}
}
- Thread.sleep() 메소드를 호출한 스레드는 주어진 시간 동안 일시 정지 상태가 되고, 다시 실행 대기 상태로 돌아간다.
- 매개값에는 얼마 동안 일시 정지 상태로 있을것인지, 밀리세컨드(1/1000)단위로 시간을 준다.
- 일시 정지 상태에서 주어진 시간이 되기전에 interrupt()가 호출되면 InterruptedException이 발생하므로 예외 처리를 한다.
다른 스레드에게 실행 양보 yield()
다른 스레드에게 실행을 양보하고 자신은 실행 대기 상태로 가는 것이 전체적인 프로그램 성능에 도움이 된다.
이때 yield() 메소드를 호출한 스레드는 실행 대기 상태로 돌아가고, 동일한 우선 순위 또는 높은 우선순위를 갖는 다른 스레드가 실행 기회를 갖도록 한다.
public class ThreadC extends Thread{
public boolean stop = false; // 종료 플래그
public boolean work = true; // 작업 진행 여부 플래그
public void run() {
while(!stop){
if(work){
System.out.println("Thread C 작업내용");
}else{
Thread.yield();
}
}
System.out.println("Thread C 종료");
}
}
- stop이 true 되면 종료
- work가 false되면 다른 스레드에게 실행 양보
public class ThreadD extends Thread{
public boolean stop = false;
public boolean work = true;
public void run() {
while(!stop){
if(work){
System.out.println("Thread D 작업내용");
}else{
Thread.yield();
}
}
System.out.println("Thread D 종료");
}
}
public class YieldExample {
public static void main(String[] args){
ThreadC threadC = new ThreadC();
ThreadD threadD = new ThreadD();
threadC.start();
threadD.start();
try{ Thread.sleep(3000);}catch (InterruptedException e) {} ;
threadC.work = false;
try{ Thread.sleep(3000);}catch (InterruptedException e) {} ;
threadC.work = true;
try{ Thread.sleep(3000);}catch (InterruptedException e) {} ;
threadC.stop = true;
threadD.stop = true;
}
}
- ThreadA, ThreadB 모두 실행
- ThreadB만 실행
- ThreadA, ThreadB 모두 실행
- ThreadA, ThreadB 모두 종료
다른 스레드의 종료를 기다리는 join()
스레드는 다른 스레드와 독립적으로 실행하는 것이 기본이나, 다른 스레드가 종료될 때까지 기다렸다가 실행해야 하는 경우가 발생할 수도 있다. 이런 경우를 위해 Thread는 join()메소드를 제공한다.
- ThreadA가 ThreadB의 join() 메소드를 호출한다
- ThreadA는 ThreadB가 종료할 때까지 일지 정지 상태가 된다.
- ThreadB의 run()메소드가 종료되면 비로소 ThreadA는 일시 정지에서 풀려 다음 코드를 실행한다.
public class JoinExample {
public static void main(String[] args){
SumThread sumThread = new SumThread();
sumThread.start();
try{
sumThread.join();
}catch (InterruptedException e){}
System.out.println("1-100 까지의 합 :"+sumThread.getSum());
}
}
결과
1-100 까지의 합 :5050
=> 만약 try catch 구문을 주석 처리후 실행하면 1~100까지의 합은 0이 나오게 된다. 이유는 SumThread가 계산 작업을 완료하지 않은 상태에서 합을 먼저 출력하기 때문이다.
스레드간 협업 wait(), notify(), notifyAll()
경우에 따라, 두 개의 스레드를 교대로 번갈아가며 실행해야 하는 경우가 있다. 정확한 교대 작업이 필요한 경우, 자신의 작업이 끝나면 상대방 스레드를 일시 정지 상태에서 풀어주고, 자신은 정지 상태로 만든다. Object 클래스의 메소드이다.
- 이 방법의 핵심은 공유 객체에 있다.
- 공유 객체는 두 스레드 작업할 내용을 각각 동기화 메소드로 구분해 놓는다.
- 한 스레드가 작업 완료하면 notify() 메소드를 호출해 일시 정지 상태에 있는 다른 스레드를 실행 대기 상태로 만들고, 자신은 두번 작업하지 않도록 wait()를 호출하여 일시 정지 상태로 만든다.
wait() wait(long millis) wait(long millis, int naos) |
동기화(synchronized) 블록 내에서 스레드를 일시 정지 상태로 만든다. 매개값으로 주어진 시간이 지나면 자동적으로 실행 대기 상태가 된다. 시간이 주어지지 않으면 notify(), notifyAll() 메소드에 의해 실행 대기 상태로 갈 수 있다. |
notify() notifyAll() |
동기화 블록 내에서 wait()메소드에 의해 일시 정지 상태에 있는 스레드를 실행 대기 상태로 만든다. |
예제 1. ThreadA와 ThreadB가 교대로 methodA()와 methodB() 호출
두 스레드의 작업 내용을 동기화 메소드로 작성한 공유 객체
public class WorkObject {
public synchronized void methodE(){
System.out.println("ThreadE의 methodE() 작업 실행");
// 일시 정지 상태에 있는 ThreadF를 실행 대기 상태로 만듦
notify();
try {
// ThreadE를 일시 정지 상태로 만듦
wait();
}catch (InterruptedException e){
}
}
public synchronized void methodF(){
System.out.println("ThreadF의 methodF() 작업 실행");
// 일시 정지 상태에 있는 ThreadE를 실행 대기 상태로 만듦
notify();
try {
// ThreadF를 일시 정지 상태로 만듦
wait();
}catch (InterruptedException e){
}
}
}
WorkObject의 methodA
public class ThreadE extends Thread{
private WorkObject workObject;
// 공유 객체를 매개값으로 받아 필드에 저장
public ThreadE(WorkObject workObject) {
this.workObject = workObject;
}
@Override
public void run() {
// 공유 객체의 methodE를 10번 반복 호출
for(int i=0; i<10; i++){
workObject.methodE();
}
}
}
public class ThreadF extends Thread{
private WorkObject workObject;
public ThreadF(WorkObject workObject) {
this.workObject = workObject;
}
@Override
public void run() {
for(int i=0; i<10; i++){
workObject.methodF();
}
}
}
public class WaitNotifyExample {
public static void main(String[] args){
WorkObject workObject = new WorkObject();
ThreadE threadE = new ThreadE(workObject);
ThreadF threadF = new ThreadF(workObject);
threadE.start();
threadF.start();
}
}
결과
...
ThreadF의 methodF() 작업 실행
ThreadE의 methodE() 작업 실행
ThreadF의 methodF() 작업 실행
ThreadE의 methodE() 작업 실행
ThreadF의 methodF() 작업 실행
예제 2. 데이터 저장하는 스레드(생산자 스레드)가 데이터 저장하면, 데이터 소비하는 스레드(소비자 스레드)가 데이터 읽고 처리하는 교대 작업
- 생성자 스레드는 소비자 스레드가 읽기전에 새로운 데이터를 두번 생성하면 안된다 (setData() 두번 실행 x)
- 소비자 스레드는 생성자 스레드가 새로운 데이터를 생성하기 전에 이전 데이터를 두번 읽으면 안된다 (getData() 두번 실행 x)
- data 필드의 값이 null이면 생산자 스레드를 실행 대기 상태로 만들고, 소비자 스레드 일시정지
- data 필드 값이 null이 아니면 소비자 스레드 - 실행 대기, 생산자 스레드 - 일시 정지 상태
두 스레드의 작업 내용을 동기화 메소드로 작성한 공유 객체
public class DataBox {
private String data;
public synchronized String getData(){
// data 필드가 null이면 스레드를 일시 정지 상태로 만듦
if(this.data == null){
try{
wait();
}catch (InterruptedException e){}
}
String returnValue = data;
System.out.println("ConsumerThread가 읽은 데이터 :"+returnValue);
// data 필드를 null로 만들고 생산자 스레드를 실행 대기 상태로 만듦
data = null;
notify();
return returnValue;
}
public synchronized void setData(String data) {
// data 필드가 null이 아니면 생산자 스레드를 일시 정지 상태로 만듦
if(this.data != null){
try{
wait();
}catch (InterruptedException e){}
}
// data 필드에 값을 저장하고 소비자 스레드를 실행 대기 상태로 만듦
this.data = data;
System.out.println("ProducerThread가 생성한 데이터 :"+data);
notify();
}
}
생산자 스레드
public class ProducerThread extends Thread{
private DataBox dataBox;
// 공유 객체를 필드에 저장
public ProducerThread(DataBox dataBox) {
this.dataBox = dataBox;
}
@Override
public void run() {
for(int i=0; i<=3; i++){
String data = "Data-"+i;
//새로운 데이터 저장
dataBox.setData(data);
}
}
}
소비자 스레드
public class ConsumerThread extends Thread{
private DataBox dataBox;
// 공유 객체를 필드에 저장
public ConsumerThread(DataBox dataBox) {
this.dataBox = dataBox;
}
@Override
public void run() {
for(int i=0; i<=3; i++){
String data = dataBox.getData();
}
}
}
public class WaitNotifyExample2 {
public static void main(String[] args){
DataBox dataBox = new DataBox();
ProducerThread producerThread = new ProducerThread(dataBox);
ConsumerThread consumerThread = new ConsumerThread(dataBox);
producerThread.start();
consumerThread.start();
}
}
결과
ProducerThread가 생성한 데이터 :Data-0
ConsumerThread가 읽은 데이터 :Data-0
ProducerThread가 생성한 데이터 :Data-1
ConsumerThread가 읽은 데이터 :Data-1
ProducerThread가 생성한 데이터 :Data-2
ConsumerThread가 읽은 데이터 :Data-2
ProducerThread가 생성한 데이터 :Data-3
ConsumerThread가 읽은 데이터 :Data-3
스레드의 안전한 종료 (stop 플래그, interrupt())
스레드는 자신의 run() 메소드가 모두 실행되면 자동적으로 종료되나, 경우에 따라 실행 중인 스레드를 즉시 종료할 필요가 있다.
가령,
사용자는 동영상을 끝까지 보지 않고 멈춤을 요구할 수 있다. Thread는 스레드를 즉시 종료시키기 위해 stop()메소드를 제공하는데 deprecated 되었다. 이유인 즉, stop()을 사용하여 갑자기 스레드를 종료하면 스레드가 사용중이던 자원(파일, 네트워크 연결 등)들이 불안정한 상태로 남겨지기 때문이다.
그럼 어떻게 스레드를 안전하게 종료할까?
두가지 방법이 있다.
예제 1. stop 플래그를 이용하는 방법
stop 필드가 false일 경우 while문의 조건식이 true가 되어 반복 실행하지만, stop 필드가 true일 경우에는 while문의 조건식이 false가 되어 while문을 빠져나온다. 그리고 스레드가 사용한 자원을 정리하고, run()메소드가 끝나게 됨으로써 스레드는 안전하게 종료된다.
public class PrintThread1 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("실행 종료");
}
}
public class StopFlagExample {
public static void main(String[] args){
PrintThread1 printThread1 = new PrintThread1();
printThread1.start();
try{
printThread1.sleep(100);
}catch (InterruptedException e){
}
// 스레드를 종료시키기 위해 stop 필드를 true 로 변경
printThread1.setStop(true);
}
}
결과
...
실행 중
실행 중
실행 중
자원 정리
실행 종료
예제 2. interrupt() 메소드를 이용하는 방법 - Thread.sleep()
interrupt()메소드는 스레드가 일시 정지 상태에 있을 때, InterruptedException 예외를 발생 시키는 역할을 한다. 이것을 이용하여 run() 메소드를 정상 종료 시킬 수 있다.
가령,
ThreadA가 ThreadB를 생성해서 start() 메소드로 ThreadB를 실행했다고 가정하자.
- ThreadA가 ThreadB의 interrupt() 메소드를 실행
- ThreadB가 sleep()메소드로 일시 정지 상태가 될 때, ThreadB는 InterruptedException이 발생하여 예외처리 catch= 블록으로 이동한다.
- 결국 ThreadB는 while문을 빠져나와 run()메소드를 정상 종료하게 된다.
public class PrintThread2 extends Thread{
@Override
public void run() {
try{
while (true){
System.out.println("실행 중");
Thread.sleep(1);
}
}catch (InterruptedException e){
}
System.out.println("자원 정리");
System.out.println("실행 종료");
}
}
public class InterruptSample {
public static void main(String[] args){
Thread thread = new PrintThread2();
thread.start();
try{
Thread.sleep(1000);
}catch (InterruptedException e){
}
// 스레드를 종료시키기 위해 InterruptedException를 발생시킴
thread.interrupt();
}
}
결과
...
실행 중
실행 중
실행 중
자원 정리
실행 종료
=> 주목할 점은, 스레드가 실행 대기 또는 실행 상태에 있을 때 interrupt() 메소드가 실행되면 즉시 InterruptedException 예외가 발생하지 않고, 스레드가 미래에 일시 정지 상태가 되면 InterruptedException 예외가 발생한다는 것이다.
따라서, 스레드가 일시 정지 상태가 되지 않으면 interrupt() 메소드는 아무런 의미가 없다.
그렇기 때문에 짧은 시간이나마 일시 정지시키기 위해 Thread.sleep(1)을 사용했다.
예제 3. interrupt() 메소드를 이용하는 방법 - interrupted()
일시 정지를 만들지 않고도 interrupted() 메소드를 사용하여 interrupt() 호출 여부를 알 수 있다.
boolean status = Thread.interrupted(); // 정적 메소드로 현재 스레드가 interrupted되었는지 확인
boolean status = objThread.isInterrupted(); // 인스턴스 메소드로 현재 스레드가 interrupted되었는지 확인
public class PrintThread2 extends Thread{
@Override
public void run() {
while (true){
System.out.println("실행 중");
if(Thread.interrupted()){
break;
}
}
System.out.println("자원 정리");
System.out.println("실행 종료");
}
}
결과
...
실행 중
실행 중
실행 중
자원 정리
실행 종료
참고하면 좋을 이전 포스팅
참고
이것이 자바다 by 신용권
'JAVA > about java' 카테고리의 다른 글
[thread] 스레드의 라이프 사이클이란 무엇인가 (0) | 2023.09.05 |
---|---|
[thread] 스레드 우선순위와 동기화를 하는 이유 (0) | 2023.08.31 |
[thread] 멀티 스레드와 작업 스레드 생성 방법 (0) | 2023.08.31 |
[java] 열거타입 enum (0) | 2023.08.30 |
[java] equals(), hashcode(), toString() 재정의를 왜 하는가 (0) | 2023.08.24 |