[Java] 스레드의 실행제어

Jeini·2023년 3월 18일
0

☕️  Java

목록 보기
59/59
post-thumbnail

⭐️ 스레드의 스케줄링과 관련된 메서드


스레드의 실행은 제어(스케줄링)할 수 있는 메서드가 제공된다.
이 들을 활용해서 보다 효율적인 프로그램을 작성할 수 있다.

❗️ resume(), stop(), suspend()스레드를 교착상태로 만들기 쉽기 때문에 depraecated 되었다.

💡 sleep()


✔️ 지정된 시간동안 스레드를 멈추게 함

sleep() 에 의해 일시정지 상태가 된 스레드는 지정된 시간이 다 되거나 interrupt() 가 호출되면 InterruptedException 이 발생하고 잠에서 깨어나 실행대기 상태가 된다.

그래서 sleep() 을 호출할 때는 항상 try-catch문으로 예외처리해야 한다.

❗️ 매번 예외처리를 해주는 것이 번거롭기 때문에, try-catch문까지 포함하는 새로운 메서드를 만들어서 사용하기도 한다.

void delay(long millis) {
	try {
    	Thread.sleep(millis);
    } catch(InterruptedException) {}
}

✏️ sleep() 활용

public class SleepEx {
    public static void main(String[] args) {
        Thread th1 = new Thread() {
            @Override
            public void run() {
                for(int i = 0; i < 300; i++) {
                    System.out.print("-");
                }
                System.out.print("\nth1 종료");
            }
        };

        Thread th2 = new Thread() {
            @Override
            public void run() {
                for(int i = 0; i < 300; i++) {
                    System.out.print("|");
                }
                System.out.println("\nth2 종료");
            }
        };

        th1.start();
        th2.start();

        try {
            th1.sleep(2000);
        } catch (InterruptedException e) {}
        System.out.println("main 종료");
    }
}
||||||||||||||||||||||||||||||||--------------||||-------------------||||||-----------------------||||||||----||||-----||||||-----------||||||||---|----||||||||||||||||||--||||||||------||||||||||||||--|||||||||-----|||||||||---------||||---------|||||||||||||||||||||||||||||||-||---|||----|||||--||-----||||||||||||||||||------------------------------------------------------|||||||--||||-|||||-----||||||------------------------|||||||||||||-------------------|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||----------------------------------------------------------------
th1 종료||||||||||||||
th2 종료
main 종료

위에 결과를 보면 스레드 th1의 작업이 가장 먼저 종료되었고, 그 다음이 th2, main순인 것을 알 수 있다.

try {
      th1.sleep(2000); // 수정 전
   } catch (InterruptedException e) {}
   System.out.println("main 종료");

하지만 th1.sleep(2000) 을 썼음에도 th1이 제일 먼저 종료되었다.

🤔 이유

  1. sleep() 은 항상 현재 실행 중인 스레드에 대해 작동
  2. 실제로 영향을 받는 것을 main메서드를 실행하는 main스레드

➡️ 따라서 sleep() 은 static으로 선언되어 있으며 참조변수를 이용해서 호출하기 보다는 Thread.sleep(2000); 과 같이 해야 함.

✏️ 수정

try {
      Thread.sleep(2000); // 수정 후
   } catch (InterruptedException e) {}
   System.out.println("main 종료");

💡 join()


✔️ 다른 스레드가 종료할 때까지 기다림

스레드 자신이 하던 작업을 잠시 멈추고 다른 스레드가 지정된 시간동안 작업을 수행하도록 할 때 join() 을 사용한다.

시간을 지정하지 않으면, 해당 스레드가 작업을 모두 마칠 때까지 기다리게 된다.
작업 중에 다른 스레드의 작업이 먼저 수행되어야할 필요가 있을 때 join() 을 사용한다.

try {
	th1.join(); // 현재 실행중인 스레드가 스레드 th1의 작업이 끝날때까지 기다린다.
} catch(InterruptedException e) {}
  • interrupted 에 의해 대기상태에서 벗어날 수 있다.
  • sleep() 과 유사한 점이 많지만, join() 은 현재 스레드가 아닌 특정 스레드에 대해 동작하므로 static 메서드가 아니다.

스레드는 다른 스레드와 독립적으로 실행되는 것이 일반적이지만, 다른 스레드가 종료될 때까지 기다리다가 실행되야되는 경우가 발생할 수 있다.

예를 들어 B라는 스레드는 계산하는 스레드이고, A라는 스레드는 B스레드가 계산이 끝나면 그 결과값을 가지고 로직을 처리해야하는 상황이다.

이러한 상황을 join() 을 이용해서 구현할 수 있다.

✏️ join() 활용

class SumThread extends Thread {
    private long sum;

    public long getSum() {
        return sum;
    }

    public void setSum(long sum) {
        this.sum = sum;
    }

    @Override
    public void run() {
        for(int i = 1; i < 100; i++) {
            sum += i;
        }
    }
}

public class JoinExample {
    public static void main(String[] args) {
        SumThread sumThread = new SumThread();
        sumThread.start();

        try {
            sumThread.join(); // sumThread가 종료될 때까지 메인 스레드를 정지시킴
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("1 ~ 100까지의 합: " + sumThread.getSum());
    }
}
1 ~ 100까지의 합: 4950

Main Thread에서는 SumThread 에서의 계산이 끝난 결과를 찍어줘야 하므로, MainThread에서 SumThread.join() 메서드를 호출하는 것을 볼 수 있다.

💡 interrupt() & interrupted()


✔️ 스레드의 작업을 취소

📎 void interrupt()
: 스레드의 interrupted상태를 false에서 true로 변경

📎 boolean inInterrupted()
: 스레드의 interrupted상태를 반환

📎 static boolean interrupted()
: 현재 스레드의 interrupted상태를 반환 후, false로 변경

진행 중인 스레드의 작업이 끝나기 전에 취소시켜야할 때가 있다.

  • 📂 큰 파일을 다운로드 받을 때 시간이 너무 오래 걸리면 중간에 다운로드를 포기하고 취소할 수 있어야 하는 경우
    ➡️ interrupt() 는 스레드에게 작업을 멈추라고 요청

하지만 단지 멈추라고 요청만 하는 것일 뿐 스레드를 강제로 종료시키지는 못한다.

interrupt() 는 그저 스레드의 interrupted상태를 바꾸는 것일 뿐이다.
그리고 interrupted() 는 스레드에 대해 interrupt()가 호출되었는지 알려준다.

interrupt() 호출 ❌ ➡️ false
interrupt() 호출 ⭕️ ➡️ true

Thread th = new Thread();
th.start();
...
th.interrupt(); // 스레드 th에 interrupt()를 호출한다.

class MyThread extends Thread {
	public void run() {
    	while(!interrupted()) {
        	...
        }
    }
}

interrupt() 가 호출되면, interrupted() 의 결과가 false 에서 true 로 바뀌어 while()문을 벗어나게 된다.

❗️ isInterrupted() 도 스레드의 interrupt() 가 호출되었는지 확인하는데 사용할 수 있지만, 스레드의 interrupted상태를 false로 초기화하지 않는다.

🧭 순서

스레드가 sleep() , wait() , join() 에 의해 일시정지 상태(WAITING)에 있음 ➡️ interrupt() 호출 ➡️ InterruptedException 발생 후 실행대기 상태(RUNNABLE)로 바뀜

📌 즉, 멈춰있던 스레드를 깨워서 실행가능한 상태로 만드는 것

✏️ interrupt 예제

import javax.swing.*;

public class InterruptEx {
    public static void main(String[] args) {
        Thread th1 = new Thread() {
            @Override
            public void run() {
                int i = 10;

                while(i != 0 && !isInterrupted()) {
                    System.out.println(i--);
                    // 시간 지연
                    for(long j = 0; j < 2500000000L; j++) {}
                }
                System.out.println("카운트가 종료되었습니다.");
            }
        };
        th1.start();

        String input = JOptionPane.showInputDialog("아무 값이나 입력하세요.");
        System.out.println("입력하신 값은 " + input + "입니다.");
        th1.interrupt(); // interrupt()를 호출하면, interrupted상태가 true가 된다.
        System.out.println("isInterrupted(): " + th1.isInterrupted()); // true
    }
}
10
9
8
7
6
5
입력하신 값은 안녕입니다.
isInterrupted(): true
카운트가 종료되었습니다.

사용자의 입력이 끝나자 interrupt() 에 의해 카운트다운이 중간에 멈추었다.

 while(i != 0 && !isInterrupted()) {
        System.out.println(i--);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            System.out.println("interrupt!");
        }
    }
    System.out.println("카운트가 종료되었습니다.");

for(long j = 0; j < 2500000000L; j++) {} 이 부분을 Thread.sleep(1000); 이렇게 바꾸었더니 멈추지 않았다.

그 이유는 Thread.sleep(1000); 에서 InterruptedException이 발생했기 때문이다.

sleep() 에 의해 스레드가 잠시 멈춰있을 때, interrupt() 를 호출하면 InterruptedException이 발생되고 스레드의 interrupted상태는 false로 자동 초기화된다.

while(i != 0 && !isInterrupted()) {
        System.out.println(i--);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            System.out.println("interrupt!");
            interrupt(); // 추가
        }
    }
    System.out.println("카운트가 종료되었습니다.");

catch 블럭에 interrupt() 를 추가로 넣어줘서 상태를 true로 다시 바꿔줘야 한다.

✏️ interrupt 예제2

class Dummy {
    public synchronized void todo() {
        System.out.println("start...");
        try {
            wait();
        } catch (InterruptedException e) {
            System.out.println("interrupted!!");
            System.out.println("th1(run): " + Thread.currentThread().isInterrupted());
        }
    }
}

public class InterrupEx {
    public static void main(String[] args) {
        Dummy d = new Dummy();

        Thread th1 = new Thread() {
            @Override
            public void run() {
                d.todo();
                System.out.println("th1 dead");
            }
        };
        th1.start();

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {}
        th1.interrupt();
        System.out.println("th1(main): " + th1.isInterrupted());

    }
}
start...
interrupted!!
th1(run): false
th1 dead
th1(main): false

th1에게 interrupt() 를 호출하여 InterruptedException이 발생되고 th1 스레드의 interrupted상태는 false로 초기화되는 것을 확인할 수 있다.

💡 yield()


✔️ 남은 시간을 다음 스레드에게 양보하고, 자신(현재 스레드)은 실행대기함

  • 실행중인 스레드를 RUNNABLE 상태로 바꾸는 메서드
  • yield()interrupt() 를 적절히 사용하면, 응답성과 효율을 높일 수 있음

📌 예시
☃️ 추운 겨울, 지훈이는 민정이와 2시와 3시 사이에 백화점 앞에서 만나기로 약속을 잡았다. 2시에 백화점 앞에 도착한 지훈이는 민정이가 정확히 2시 몇분에 도착할 지 모른다. 그래서 지훈이는 추운 겨울 밖에서 기다리기 보다는 잠시 백화점 안의 카페에 들어가 몸을 녹이며 기다리기로 결정한다.

이와 같이, 스레드가 실행되었는데 스레드의 실행이 잠시동안 무의미한 경우가 존재한다. 이런 경우 스레드를 잠시 실행 대기 상태로 돌려놓으면 좋다.
그래야 CPU의 자원의 소모를 방지할 수 있다.


References
: https://lordofkangs.tistory.com/29
: https://cafe.naver.com/javachobostudy

profile
Fill in my own colorful colors🎨

0개의 댓글