프로그래밍 언어/Java

[Java] 자바 쓰레드 정리

happy_life 2022. 7. 3. 17:46

자바  쓰레드 정리

 

프로세스와 쓰레드

프로세스는 실행 중인 프로그램이다. 프로그램을 실행하면 OS로부터 자원을 받아 프로세스가 된다. 이런 프로세스 안에는 데이터, 메모리 등의 자원과 쓰레드가 존재한다. 프로세스 안의 자원을 활용해 실제 작업을 수행하는 게 바로 쓰레드이다. 프로세스에는 최소 하나 이상의 쓰레드가 존재하고, 여러 개의 쓰레드를 가진 프로세스를 "멀티 쓰레드" 프로세스라고 한다.

프로세스는 공장, 쓰레드는 일꾼이라고 이해하면 쉽다.

 

 

쓰레드의 구현

 쓰레드를 구현하기 위해 두 가지 방식이 있다. Thread 클래스를 상속받는 방법, Runnable 인터페이스를 구현하는 방법이다. 자바에서는 부모가 하나뿐이므로, 멀티 쓰레드도 사용하고, 다른 클래스도 유연하게 상속받기 위해 인터페이스를 구현하는 방법이 일반적으로 사용된다.

 

Thread 클래스를 상속

class MyThread extends Thread {
    public void run() {

    }
}

 

Runnable 인터페이스 구현

class MyThread2 implements Runnable {

    @Override
    public void run() {

    }
}

 

 

아래의 예제를 통해 두 가지 방식의 차이를 이해해보자.

 

코드 예제

public class ThreadEx1 {
    public static void main(String[] args) {
        //Thread 클래스 상속
        MyThread myThread = new MyThread();

        //Runnable 인터페이스 구현
        Runnable myThread2 = new MyThread2();
        Thread thread2 = new Thread(myThread2);

        // 쓰레드 시작
        myThread.start();
        thread2.start();


    }
}

class MyThread extends Thread {
    public void run() {
        for (int i = 0; i < 15; i++) {
            System.out.println(getName()); // 부모 Thread의 getName()호출
        }
    }
}

class MyThread2 implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 15; i++) {
            System.out.println(Thread.currentThread().getName()); // 부모 Thread의 getName()호출
        }
    }
}

 

위의 코드에서 보듯 Runnable 인터페이스를 구현한 경우 인스턴스를 생성한 다음 Thread클래스의 생성자에 매개변수로 제공해야 한다.

 

그리고 getName()같은 경우도 Thread를 상속받으면 자식 클래스에서 조상 클래스의 메서드를 직접 호출할 수 있지만, Runnable을 구현한 경우 Thread의 currentThread()를 호출해 쓰레드에 대한 참조를 얻어와야 호출이 가능하다.

이 코드는 아래와 같은 것으로 이해하면 된다.

Thread t = Thread.currentThread();
String name = t.getName();
System.out.println(name);

 

 

쓰레드의 실행

start()

쓰레드를 생성하고 start() 메서드를 호출해야 쓰레드가 실행된다. 그리고 또한 start() 코드가 실행되었다고 해서 바로 실행되는 것은 아니다. 쓰레드의 실행은 OS 스케줄러의 로직에 의해 결정되는 것이기 때문이다. 또한 종료된 쓰레드는 다시 start()메서드를 사용해 다시 호출할 수 없다.

 

start()와 run()

쓰레드의 내용을 실행하는건 run()인데 왜 start() 메서드만 호출될까? 이에 대한 의문을 해결해보자.

 

main메서드에서 run()을 호출하는 것은 생성된 쓰레드를 실행하는 것이 아니라, 단순히 클래스에 선언된 메서드를 호출하는 것일 뿐이다. 하지만 start()는 main()처럼 독립된 쓰레드 호출스택을 생성해 run()을 호출하게 된다.

start() 동작과정

 

  1. main 메서드에서 쓰레드의 start()를 호출한다.
  2. start()가 새로운 쓰레드를 생성하고 새로운 쓰레드 호출 스택을 생성한다.
  3. run()이 호출되어 쓰레드가 독립된 공간에서 수행된다.
  4. 쓰레드가 이제 2개 이므로 CPU 스케줄러에 의해 번갈아 가며 실행된다.

 

 

쓰레드의 상태

쓰레드는 생성부터 소멸까지의 생명주기를 가진다. 생명주기 안에서 쓰레드의 각 상태에 대해 알아보자.

상태 설명
NEW 쓰레드가 생성되고 start()가 호출되지 않은 상태
RUNNABLE 실행 혹은 실행중 상태
BLOCKED 동기화 블럭에 의해 일시정지된 상태
WAITING, TIMED_WAITING 일시정지된 상태, TIMED는 일시정지 시간이 정해진 것을 의미
TERMINATED 쓰레드의 작업이 종료된 상태

 

 쓰레드를 생성하고 start()를 호출하면, 바로 실행되는 것이 아니라, 실행대기 큐에 순서대로 대기하게 된다. 이후 본인의 차례가 오면 실행하게 되고 특정 요인에 의해 일시 정지가 되기도 하고 종료되기도 한다.

 

쓰레드의 상태제어 메서드

메서드 설명
sleep() 지정된 시간동안 쓰레드를 일시정지시킨다. 시간이 지나면 자동적으로 다시 실행 대기상태가 된다.
join() 지정된 시간동안 특정 쓰레드가 작업하는 것을 기다린다.
interrupt() sleep()이나 join()에 의해 일시정지인 쓰레드를 꺠워 실행 대기상태로 만든다.
stop() 해당 쓰레드를 즉시 종료시킨다. 
suspend() 쓰레드를 일시정지 시킨다. resume()과 관련있다.
resume() suspend()에 의해 일시정지한 쓰레드를 실행 대기상태로 만든다.
yield() 자신의 CPU할당을 다른 쓰레드에 넘기고 스스로 실행 대기상태가 된다.

 

sleep()

static void sleep(long millis) //천분의 1초단위

의미

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

 

특징

예외처리를 해야한다.( InterruptedException이 발생하면 깨어난다). interrupt가 발생하는 경우 예외가 발생하여 sleep 상태를 벗어나게 된다.

 

코드 예제

try {
    Thread.sleep(5000);
} catch (InterruptedException e) {
}

 

interrupt()

void interrupt() // 쓰레드의 interrupted 상태를 false에서 true로 변경
boolean isInterrupted() // 쓰레드의 interrupted 상태를 반환.
static boolean interrupted() // 현재 쓰레드의 iterrupted상태를 알려주고 false로 초기화

의미

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

 

코드 예제

public class ThreadEx5 {
    public static void main(String[] args) {
        Thread1234 thread1234 = new Thread1234();
        thread1234.start();
        // Interrupted 된 적이 있는지 체크
        System.out.println("thread1234.isInterrupted() = " + thread1234.isInterrupted());

        //main 쓰레드에서 Dialog띄우기
        JOptionPane.showInputDialog("값을 입력해주세요.");

        // Dialog에 입력하고 난 후 thread1234에 interrupt 주기
        thread1234.interrupt();

        // Interrupted 된 적이 있는지 체크
        System.out.println("thread1234.isInterrupted() = " + thread1234.isInterrupted());

        // thread1234가 아닌 main 쓰레드가 interrupted된 적 있는지 체크
        System.out.println(Thread.interrupted());
//        System.out.println(thread1234.interrupted());
    }
}

class Thread1234 extends Thread{
    public void run() {
        int i = 10;

        while (i != 0 && !isInterrupted()) {
            System.out.println(i--);
            // 시간 지연
            for(long x=0;x<2500000000L;x++);
            }
        }
    }

실행 결과

 

 

join()

void join() // 작업이 모두 끝날 때까지
void join(long millis) // 천분의 일초동안

의미

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

 

특징

sleep()과 마찬가지로 작업이 끝나면 예외를 발생시켜 종료하는 메커니즘을 활용한다.

 

코드 예제

public class ThreadEx6 {
    public static void main(String[] args) {
        AThread aThread = new AThread();
        BThread bThread = new BThread();
        aThread.start();
        bThread.start();

        long startTime = System.currentTimeMillis();

        try {
            aThread.join(); // main쓰레드가 aThread 작업 끝까지 기다린다.
            bThread.join(); // main쓰레드가 bThread 작업 끝까지 기다린다.
        } catch (InterruptedException e) {}

        System.out.println("소요시간: " + (System.currentTimeMillis()-startTime));
    }
}

class AThread extends Thread{
    public void run() {
        for (int i = 0; i < 300; i++) {
            System.out.print("-");
        }
    }//run
}

 

실행 결과

 

만약 join()메서드가 없었다면 소요시간: 0을 먼저 출력했을 것이다. 하지만 join()메서드로 인해 aThread와 bThread가 모두 동작 한 다음에서야 main 쓰레드의 아래 코드가 실행되어 16이 출력된 것이다.

 

 

Q) 왜 - | 가 사이사이에 있나?

main 쓰레드가 aThread와 bThread 다음에 실행되어야 하는것이지 aThread와 bThread 사이의 관계에서 join()을 사용한 것은 아니기 때문이다.

 

 

yield()

static void yield() // 다음 쓰레드에게 양보하고 자신은 대기한다.

의미

남은 시간을 다음 쓰레드에게 양보하고, 자신(현재 쓰레드)는 실행 대기한다.

 

특징

yield는 OS 스케줄러에게 통보하는 것으로 반드시 양보한다는 것을 보장하지 않는다.

 

코드 예제

class ThreadEx7_1 implements Runnable {
    boolean stopped = false;
    boolean suspended = false;

    Thread th;

    ThreadEx7_1(String name) {
        th = new Thread(this, name);
    }

    @Override
    public void run() {
        while (!stopped) {
            if (!suspended) {
                /**
                 * 작업 수행
                 */
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                }
            } else {
                Thread.yield(); // 양보 -> OS에 통보
            }//if
        }//while
    }
}

위의 코드에서 stopped 는 false인데 suspended가 true가 되었다고 생각해보자. 이런경우 if 문 밖에서 계속 while문을 돌 게될 것이다. 이렇게 도느니 차라리 다른 쓰레드에 CPU를 양보하면 더 효율적이지 않을까? 그래서 else문에 yield()를 사용하게 되는 것이다.

 

 

아래의 3가지 (suspend, resume, stop)는 교착상태에 빠지기 쉬워 deprecated 되었으니 참고만 하자.

suspend()

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

의미

쓰레드를 일시정지 시킨다.

 

resume()

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

의미

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

 

stop()

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

의미

쓰레드를 즉시 종료시킨다.

 

 

 

 

멀티쓰레드의 장점과 단점

장점

멀티쓰레드의 장점
CPU 사용률을 높인다.
자원을 보다 효율적으로 사용할 수 있다. 새로운 프로세스를 생성하는 것보다 새로운 쓰레드를 생성하는 것이 자원과 시간을 덜 소비한다.
사용자에 대한 응답성이 향상된다.
작업이 분리되어 코드가 간결해진다.

메신저 채팅앱을 생각해보자. 만약 싱글 쓰레드로 되어있다면, 사용자가 파일을 보내는 도중에 채팅 등 다른 일을 전혀할 수 없다. 하지만 멀티 쓰레드로 되어 있다면, 파일을 보내는 것은 한 쓰레드가 하고, 채팅은 다른 쓰레드가 대응함으로써, 동시에 여러가지 일을 할 수 있게 된다.

 

단점

여러 쓰레드가 자원을 공유하기 때문에 동기화, 교착상태같은 문제들이 발생할 수 있다.

 

데몬 쓰레드

 데몬 쓰레드는 다른 일반 쓰레드를 돕는 보조적인 쓰레드이다. 데몬 쓰레드는 다른 일반 쓰레드가 모두 종료되면 자동으로 같이 종료된다.

 

특정한 쓰레드를 데몬 쓰레드로 만들기 위해서는 setDaemon()에 인자로 true를 넣어주고 실행하면 된다.

 

코드 예제

public class ThreadEx3 implements Runnable{
    static boolean autoSave = false;

    public static void main(String[] args) {
        Thread demonThread = new Thread(new ThreadEx3());
        demonThread.setDaemon(true); // 데몬 쓰레드로 설정
        demonThread.start(); // 데몬 쓰레드 시작

		// 메인 쓰레드
        for (int i = 1; i <= 10; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {}
            System.out.println(i);

            if (i == 5) {
                autoSave = true;
            }
        }
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(1000 * 3);
            } catch (Exception e) {}
            if (autoSave) {
                System.out.println("파일이 저장되었습니다.");
            }
        }
    }
}