Java의 정석_기초편

쓰레드(thread)

DJDU 2022. 11. 10. 16:49

프로세스와 쓰레드(Process & thread)

  • 프로세스 : 실행중인 프로그램, 자원(resources)과 쓰레드로 구성
    쓰레드 : 프로세스 내에서 실제 작업을 수행.
  • 모든 프로세스는 최소한 하나의 쓰레드를 갖고 있다.
  • 싱글 쓰레드 프로세스 = 자원 + 쓰레드
    멀티 쓰레드 프로세스 = 자원 + 쓰레드 + 쓰레드 + ... + 쓰레드
  • 2 프로세스 1 쓰레드 vs 1 프로세스 2 쓰레드
더보기

프로세스 : 실행중인 프로그램, 자원(resources)과 쓰레드로 구성
쓰레드 : 프로세스 내에서 실제 작업을 수행

  • 자원(resources)는 '메모리', 'CPU' 등의 다른 컴퓨터 디바이스 등

 

모든 '프로세스'는 최소한 하나의 '쓰레드'를 갖고 있다.

싱글 쓰레드 프로세스 = 자원 + 쓰레드
멀티 쓰레드 프로세스 = 자원 + 쓰레드 + 쓰레드 + ... + 쓰레드

 

활성 상태 보기(윈도우 : 작업 관리자)

2 프로세스 1 쓰레드 vs 1 프로세스 2 쓰레드

"하나의 새로운 프로세스를 생성하는 것보다 하나의 새로운 쓰레드를 생성하는 것이 더 적은 비용이 든다."

2 프로세스 1 쓰레드는 싱글 쓰레드 프로세스가 2개 있는 것이다. 1 프로세스 2 쓰레드는 멀티 쓰레드 프로세스가 1개이다.

 

멀티쓰레드의 장단점

  • 대부분의 프로그램이 멀티쓰레드로 되어 있다. 그러나, 멀티쓰레드 프로그래밍이 장점만 있는 것은 아니다.
더보기

대부분의 프로그램이 멀티쓰레드로 되어 있다.
그러나, 멀티쓰레드 프로그래밍이 장점만 있는 것은 아니다.

 

쓰레드의 구현과 실행

  1. Thread클래스를 상속
  2. Runnable인터페이스를 구현
더보기

1. Thread클래스를 상속

class MyThread extends Thread {
    public void run() {  // Thread클래스의 run()을 오버라이딩
        /* 작업내용 */
    }
}
MyThread t1 = new MyThread();  // 쓰레드의 생성
t1.start();  // 쓰레드의 실행

Thread클래스를 상속한 경우 위와 같이 쓰레드를 생성하고 실행하면 된다.

2. Runnable인터페이스를 구현

class MyThread2 implements Runnable {
    public void run() {  // Runnable인터페이스의 추상메서드 run()을 구현
        /* 작업내용 */
    }
}

 클래스를 상속받는 방법 보다 인터페이스를 구현하는 방법이 ''측면에서 더 낫다. 자바는 단일상속만 허용하기 때문에 Thread를 상속받으면 다른 클래스를 상속받기 어렵다. 반면 인터페이스를 구현하면 다른 클래스를 상속받을 수 있다.

public interface Runnable {
    public abstract void run();
}

 Runnable인터페이스는 위와 같이 정의되어 있다. run이라는 이름의 추상메서드 하나만 멤버로 가지고 있기 때문에, 추상메서드만 완성하면 된다. run()의 '작업 내용'을 완성해야 한다는 점에서는 두 방법이 모두 동일하다. 

main() { /* 작업내용 */ }

 여태까지 main메서드 안에 작업할 내용을 작성했듯이, run메서드도 똑같이 하면 된다. 메서드 이름만 run일 뿐이다. 

Runnable  r = new MyThread2();
Thread   t2 = new Thread(r);  // Thread(Runnable r)
// Thread t2 = new Thread(new MyThread2());  // 1, 2 == 3
t2.start();

 

예제13-1

  • 코드
더보기

멀티 쓰레드 코드

class Ex13_1 {
    public static void main(String args[]) {
        ThreadEx1_1 t1 = new ThreadEx1_1();

        Runnable r = new ThreadEx1_2();
        Thread t2 = new Thread(r);	  // 생성자 Thread(Runnable target)

        t1.start();  // 0을 출력
        t2.start();  // 1을 출력
    }
}

class ThreadEx1_1 extends Thread {  // 1. Thread클래스를 상속해서 쓰레드를 구현
    public void run() {  // 쓰레드가 수행할 작업을 작성
        for(int i=0; i < 500; i++) {
            System.out.print(0); // 조상인 Thread의 getName()을 호출
        }
    }
}

class ThreadEx1_2 implements Runnable {  // 2. Runnable인터페이스를 구현해서 쓰레드를 구현
    public void run() {  // 쓰레드가 수행할 작업을 작성
        for(int i=0; i < 500; i++) {
            // Thread.currentThread() - 현재 실행중인 Thread를 반환한다.
            System.out.print(1);
        }
    }
}

결과

두 쓰레드의 작업이 비동기적으로 진행된 것을 알 수 있다.

싱글 쓰레드 코드

class Ex13_1 {
    public static void main(String args[]) {
        for(int i=0; i < 500; i++) {
            System.out.println(0);
        }

        for(int i=0; i < 500; i++) {
            System.out.println(1);
        }
    }
}

결과

동기적으로 작업이 진행되는 것을 알 수 있다.

 

쓰레드의 실행 - start()

  • 쓰레드를 생성한 후에 start()를 호출해야 쓰레드가 작업을 시작한다.
더보기

쓰레드를 생성한 후에 start()를 호출해야 쓰레드가 작업을 시작한다.

  1. 쓰레드를 생성
  2. start() 호출
ThreadEx1_1 t1 = new ThreadEx1_1();  // 쓰레드 t1을 생성한다.
ThreadEx1_1 t2 = new ThreadEx1_1();  // 쓰레드 t2을 생성한다.

t1.start();  // 쓰레드 t1을 실행시킨다.
t2.start();  // 쓰레드 t2을 실행시킨다.

2. start() 호출

t1을 start()로 먼저 실행하고, t2를 start()로 실행했지만, start()를 호출했다고 바로 실행되는 건 아니다. 또한, start()를 먼저 호출한 쓰레드가 꼭 먼저 실행되는 건 아니다. 어느 쓰레드가 먼저 실행될 지 모른다. 왜냐하면, start()를 실행하면 실행가능한 상태가 되는 것이지, 바로 실행되는 것이 아니기 때문이다. 

 OS스케쥴러가 실행 순서를 결정한다. 예를 들어, 윈도우즈면 윈도우즈 스케쥴러가 있다. OS스케쥴러가 그렇다면 그런 것이지, 한낱 JVM따위가 이래라 저래라 할 수 없는 문제이다. 😥 이걸 유식하게 표현하면, 쓰레드는 OS스케쥴러에 의존적이다.

 

start()와 run()

더보기

start()와 run()

run을 구현했는데 왜 run이 나니, start를 호출할까?

 

main메서드에서 start를 호출하면 호출 스택(Call Stack)이 위와 같다.

start메서드는 새로운 호출 스택을 생성한다. 호출스택이 생성된 후, run메서드가 실행되면 start()는 실행 종료된다.

이후, 각각의 쓰레드가 자신만의 호출스택을 가지고 서로 독립적인 작업을 수행한다.

 

 start가 아닌 run()을 실행한다면, 하나의 호출 스택을 하나의 쓰레드가 실행하는 꼴이 된다. start메서드를 실행해야 새로운 스택이 생기고, 거기에 run()이 올라가 쓰레드가 실행되는 것이다.

run()을 실행하면 안 되는 이유

 

main쓰레드

  • main메서드의 코드를 수행하는 쓰레드
  • 쓰레드는 '사용자 쓰레드'와 '데몬 쓰레드' 두 종류가 있다.
더보기

main메서드의 코드를 수행하는 쓰레드

public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello, world");
    }
}

위와 같은 프로그램을 실행한다고 할 때, 

> java Hello.java

cmd상에서 Hello.java를 실행한다.

그 결과, 새로운 호출 스택이 만들어지고, 쓰레드가 main메서드에 있는 코드를 순서대로 실행한다. 이 쓰레드(일꾼)이 바로 main쓰레드이다.

쓰레드는 '사용자 쓰레드'와 '데몬 쓰레드' 두 종류가 있다.

  • main쓰레드는 '사용자 쓰레드'이고, 데몬 쓰레드는 '보조 쓰레드'이다.
중요합니다.

 

예제13-11

  • 코드
더보기

결과

 

join메서드

  • main쓰레드가 다른 쓰레드의 작업이 끝날 때 까지 기다리게 하는 메서드
try {
    th1.join();	// main쓰레드가 th1의 작업이 끝날 때까지 기다린다.
    th2.join();	// main쓰레드가 th2의 작업이 끝날 때까지 기다린다.
} catch(InterruptedException e) {}

위 코드를 주석처리 하고 다시 실행해보자.

결과

| 참고 | join()을 호출하지 않고 테스트했음에도 main메서드가 제일 마지막에 끝나서, 쓰레드들의 반복 횟수를 1000으로 늘렸다.

결론

main메서드가 종료되어도 다른 쓰레드가 종료되지 않으면, 프로그램이 종료되지 않는다. ⭐️

 

싱글쓰레드와 멀티쓰레드

  • 싱글쓰레드
  • 멀티쓰레드

 

예제13-2, 13-3

  • 코드(13-2, 13-3)

 

쓰레드의 I/O블락킹(blocking)

  • 예제13-4(싱글쓰레드)
  • 예제13-5(멀티쓰레드)
더보기

용어 정리

  • Input : 입력
  • Output : 출력
  • I/O : 입출력
  • 블락킹 : 막힘
  • I/O블락킹 : 입출력 시 작업 중단

 

예제13-4(싱글쓰레드)

  • 입출력 시 블락킹이 발생하면 다음 작업을 진행하지 못 한다.

 

예제13-5(멀티쓰레드)

  • 입출력 시 블락킹이 발생해도 다음 작업을 진행할 수 있다. 즉, 어떤 작업이 수행되는 동안 외부적인 요인에 의해서 멈춰있을 때에도 다른 쓰레드가 작업을 수행할 수 있어서 작업을 좀 더 효율적으로 빨리 끝낼 수 있다.

 

쓰레드의 우선순위(priority of thread)

  • 작업의 중요도에 따라 쓰레드의 우선순위를 다르게 하여 특정 쓰레드가 더 많은 작업시간을 갖게 할 수 있다.
더보기

작업의 중요도에 따라 쓰레드의 우선순위를 다르게 하여 특정 쓰레드가 더 많은 작업시간을 갖게 할 수 있다.

public static final int MAX_PRIORITY  = 10  // 최대우선순위
public static final int MIN_PRIORITY  =  1  // 최소우선순위
public static final int NORM_PRIORITY =  5  // 보통우선순위

 

JVM에서는 쓰레드 우선순위는 1부터 10까지 지정할 수 있다.(WinOS: 32단계)

쓰레드 우선순위의 기본값은 5이다.

우선순위는 쓰레드가 실행된 이후에도 변경할 수 있다.

쓰레드 우선순위는 '희망사항'일 뿐이다. 실제로는 OS스케줄러에서 정한대로 실행된다.

 

예제13-6

  • 코드
더보기

시간지연용 for문

  • 특정 작업이 너무 일찍 끝나지 않게 하려고 삽입하는 for문
class ThreadEx6_1 extends Thread {
    public void run() {
        for(int i=0; i < 300; i++) {
            System.out.print("-");
            for(int x=0; x < 10000000; x++);  // 시간지연용 for문
        }
    }
}

쓰레드 우선순위 기본값 5

쓰레드 기본값이 정말 5인지 주석처리 후 실행해보자.

코드

//      th1.setPriority(5);
        th2.setPriority(7);

결과

Priority of th1(-) : 5
Priority of th2(|) : 7

 

하지만 작업 종료 시점이 항상 일정하지는 않다.(희망사항이기 때문) 확률이 높아질 뿐이다. 

그래서 '우선순위'에 크게 의존하면 안 된다.

급한 작업들이 우선순위가 높아야 겠다. 윈도우즈의 경우 마우스 포인터를 높은 우선순위로 두고 있다.

우선순위는 상대적이다.

 

| 참고 | 프로그램을 작성할 때 어떤 작업을 우선으로 둘지 고민해야 한다.

 

쓰레드 그룹

  • 서로 관련된 쓰레드를 그룹으로 묶어서 다루기 위한 것
  • 모든 쓰레드는 반드시 쓰레드 그룹에 포함되어 있어야 한다.
  • 쓰레드 그룹을 지정하지 않고 생성한 쓰레드는 'main쓰레드 그룹'에 속한다.
  • 자신을 생성한 쓰레드(부모 쓰레드)의 그룹과 우선순위를 상속받는다.
더보기

모든 쓰레드는 반드시 쓰레드 그룹에 포함되어 있어야 한다.

Thread(ThreadGroup group, String name)
Thread(ThreadGroup group, Runnable target)
Thread(ThreadGroup group, Runnable target, String name)
Thread(ThreadGroup group, Runnable target, String name, long stackSize)
ThreadGroup getThreadGroup() 쓰레드 자신이 속한 쓰레드 그룹을 반환한다.
void uncaughtException(Thread t, Throwable e) 처리되지 않은 예외에 의해 쓰레드 그룹의 쓰레드가 종료되었을 때, JVM에 의해 이 메서드가 자동적으로 호출된다.

 

쓰레드 그룹의 메서드

  • 생성자
  • 메서드
더보기

생성자

메서드

쓰레드들은 쓰레드 그룹으로 묶어서 다룰 수 있다는 점만 알아두자.

 

데몬 쓰레드(daemon thread)

  • 일반 쓰레드(non-daemon thread)의 작업을 돕는 보조적인 역할을 수행
  • 일반 쓰레드가 모두 종료되면 자동적으로 종료된다.
  • 가비지 컬렉터, 자동저장, 화면 자동갱신 등에 사용된다.
  • 무한루프와 조건문을 이용해서 실행 후 대기하다가 특정조건이 만족되면 작업을 수행하고 다시 대기하도록 작성한다.
더보기

무한루프와 조건문을 이용해서 실행 후 대기하다가 특정조건이 만족되면 작업

public void run() {
    while(true) {  // ← 무한루프
        try {
            Thread.sleep(3 * 1000);  // 3초마다
        } catch(InterruptedException e) {}
        
        // autoSave의 값이 true이면 autoSave()를 호출한다.
        if(autoSave) {
            autoSave();
        }
    }
}

 

무한루프는 영원히 반복되는 것이 아니라, 일반 쓰레드가 모두 종료되면 자동적으로 종료된다.

사실, 데몬 쓰레드를 작성하는 패턴은 이런식으로 결정되어 있다(무한루프 + if문)

 

setDaemon(boolean daemon)은 반드시 start()를 호출하기 전에 실행되어야 한다.
그렇지 않으면 IllegalThreadStateException이 발생한다.

 

예제13-7

  • Thread.sleep메서드
  • setDaemon(boolean on)
더보기

Thread.sleep메서드

  • sleep메서드가 뭔지 몰라서 찾아봤다.
  • 지정된 시간 동안 현재 스레드를 일시 중단합니다.

| 출처 | Microsoft | 설명서 | Thread.Sleep 메서드

 

setDaemon(boolean on) 테스트

  • t.setDaemon(true); 코드가 없으면 프로그램이 종료되지 않는다.
  • 이 코드가 없으면, run쓰레드가 보조 쓰레드가 되지 않고, 같은 일반 쓰레드 상태로 프로그램이 실행되기 때문이다.
  • 코드를 주석처리 하고 실행하면, 프로그램이 종료되지 않고 run쓰레드가 무한루프 때문에 영원히 실행된다.
class Ex13_7 implements Runnable  {
    static boolean autoSave = false;

    public static void main(String[] args) {
        Thread t = new Thread(new Ex13_7());  // Thread(Runnable r)
        t.setDaemon(true);		// 이 부분이 없으면 종료되지 않는다.
        t.start();

결과

1
2
3
4
5
작업파일이 자동저장되었습니다.
6
7
8
작업파일이 자동저장되었습니다.
9
10
프로그램을 종료합니다.
작업파일이 자동저장되었습니다.
작업파일이 자동저장되었습니다.
작업파일이 자동저장되었습니다.
작업파일이 자동저장되었습니다.

 

쓰레드의 상태

더보기

쓰레드의 상태

 

쓰레드의 실행제어

  • 쓰레드의 실행을 제어할 수 있는 메서드가 제공된다. 이들을 활용해서 보다 효율적인 프로그램을 작성할 수 있다.
더보기
  • 쓰레드의 실행을 제어할 수 있는 메서드가 제공된다. 
  • 이들을 활용해서 보다 효율적인 프로그램을 작성할 수 있다.

Thread 실행제어 static  - 쓰레드 자기 자신에게만 호출 가능 ⭐️ 

  1. sleep() - 내가 잠 들 수는 있지만, 남을 잠들게 할 순 없다.
  2. yield()  - 내가 양보할 수는 있지만, 남을 양보하게 만들 순 없다.

 

sleep()

  • 현재 쓰레드를 지정된 시간동안 멈추게 한다.
  • 예외처리를 해야 한다.(InterruptedException이 발생하면 깨어남)
  • 특정 쓰레드를 지정해서 멈추게 하는 것은 불가능하다.
더보기

현재 쓰레드를 지정된 시간동안 멈추게 한다.

  • 멈추게 한다 = 잠자게 한다.
  • long millis : 잠 잘 시간(천 분의 일초. 3초 == 3 * 1000)
static void sleep(long millis)            // 천분의 일초 단위
static void sleep(long millis, int nanos) // 천분의 일초 + 나노초(10^-9 잘 안 씀)

 

예외처리를 해야 한다.(InterruptedException이 발생하면 깨어남)

  • InterruptedException : Exception클래스의 자손(예외처리 필수)
try {
    Thread.sleep(1, 500000);  // 쓰레드를 0.0015초 동안 멈추게 한다.
} catch(InterruptedException e) {}

쓰레드가 멈추는 경우 

  1. time up.         시간이 다 됨
  2. interrupted    깨우는 것

delay()

  • 매번 예외처리를 하는 것은 번거롭고 귀찮다. 따라서 delay메서드를 따로 만들었다.
void delay(long millis) {
    try {
        Thread.sleep(millis);
    } catch(InterruptedException e) {}
}

예외 처리 코드를 다음과 같이 한 줄 코드로 바꿀 수 있겠다.

delay(15);

 

특정 쓰레드를 지정해서 멈추게 하는 것은 불가능하다.

  • th1.sleep(2000) : th1을 sleep()하는 게 아니다.
  • 에러는 안 나지만 이렇게 오해할 수 있으니, 반드시 클래스이름.sleep()으로 작성해야 한다(static메서드)
  • Thread.sleep(2000);

 

예제13-8

  • 코드
  • [테스트] 오해가 발생하는 이유
  • [테스트] delay메서드 사용
더보기

[테스트] 오해가 발생하는 이유

  • 프로그램을 실행하면, th1.sleep(2000);이라 작성하여도 th1쓰레드가 아닌, main쓰레드가 2초간 잠든다.

 

[테스트] delay메서드 사용

  • 일일이 예외처리를 해야 하는 번거로움을 해소하기 위해 delay메서드를 사용할 수 있다.
class Ex13_8 {
    public static void main(String args[]) {
        ThreadEx8_1 th1 = new ThreadEx8_1();
        ThreadEx8_2 th2 = new ThreadEx8_2();
        th1.start(); th2.start();

        delay(2 * 1000);
        System.out.print("<<main 종료>>");
    } // main
    
    static void delay(long millis) {
        try {
            Thread.sleep(millis);
        } catch(InterruptedException e) {} 
    }
}

 

interrupt()

  • 대기상태(WAITING)인 쓰레드를 실행대기상태(RUNNABLE)로 만든다.
더보기

대기상태(WAITING)인 쓰레드를 실행대기상태(RUNNABLE)로 만든다.

  • 대기상태(WAITING) : sleep() || join() || wait()에 의해 작업이 중단
  • interrupt()로 호출하면 쓰레드를 깨워 실행 가능한 상태(RUNNABLE)로 만든다.

 

void interrupt()                          쓰레드의 interrupted상태를 false에서 true로 변경.

boolean isInterrupted()           쓰레드의 interrupted상태를 반환.

static boolean interrupted()   현재 쓰레드의 interrupted상태를 알려주고, false로 초기화

 

public static void main(String[] args) {
    ThreadEx13_2 th1 = new ThreadEx13_2();
    th1.start();
        ...
    th1.interrupt();  // interrupt()를 호출하면, interrupted상태가 true가 된다.
        ...
    System.out.println("isInterrupted():"+ th1.isInterrupted());  // true
}
class Thread {  // 알기 쉽게 변경한 코드
        ...
    boolean interrupted = false;
        ...
    boolean isInterrupted() {
        return interrupted;
    }
    
    boolean interrupt() {
        interrupted = true;
    }
}
class ThreadEx13_2 extends Thread {
    public void run() {
            ...
        while (downloaded && !isInterrupted()) {
            // download를 수행한다.
            ...
        }
        
        System.out.println("다운로드가 끝났습니다.");
    } // main
}

 

예제13-9

더보기

[테스트] isInterrupted() 호출

 

[테스트] interrupted() 호출

  • main쓰레드가 interrupt되었는지 확인
  • interrupted()호출 시 '클래스이름.interrupted()'로 작성해야 한다.(참조변수x)
  • isInterrupted()와 달리 interrupted()는 interrupted라는 상태변수를 false로 초기화

 

suspend(), resume(), stop

  • 쓰레드의 실행을 일시정지, 재개, 완전정지 시킨다.
  • suspend(), resume(), stop()은 교착상태에 빠지기 쉬워서 deprecated되었다.
더보기

쓰레드의 실행을 일시정지, 재개, 완전정지 시킨다.

 

void suspend()      쓰레드를 일시정지시킨다.

void resume()        suspend()에 의해 일시정지된 쓰레드를 실행대기상태로 만든다.

void stop()              쓰레드를 즉시 종료시킨다.

 

suspend(), resume(), stop()은 교착상태에 빠지기 쉬워서 deprecated되었다.

class ThreadEx17_1 implements Runnable {
    boolean suspended = false;
    boolean stopped = false;
    
    public void run() {
        while(!stopped) {
            if(!suspended) {
                /* 쓰레드가 수행할 코드를 작성 */
            }
        }
    }
    public void suspend() { suspended = true; }
    public void resume()  { suspended = false; }
    public void stop()    { stopped = true; }
}

 

예제13-10

 

join()

  • 지정된 시간동안 특정 쓰레드가 작업하는 것을 기다린다.
  • 예외처리를 해야 한다(InterruptedException이 발생하면 작업 재개)
더보기

지정된 시간동안 특정 쓰레드가 작업하는 것을 기다린다.

 

예외처리를 해야 한다(InterruptedException이 발생하면 작업 재개)

 

 

예제13-11