Java - thread

Dasole Kwon·2023년 1월 19일
0

쓰레드(Thread)

프로세스와 쓰레드

프로세스(process)

  • 프로세스는 프로그램을 수행하는데 필요한 데이터와 메모리 등의 자원 그리고 쓰레드로 구성되어 있다.
  • 실제로 작업을 수행하는 것이 바로 쓰레드(thread)이다.

    프로세스(공장), 쓰레드(일꾼) : 싱글 쓰레드
    프로세스(공장), 쓰레드들(일꾼들) : 멀티 쓰레드

쓰레드를 가벼운 프로세스, 즉 경량 프로세스(LWP, light-weight process)라고 부르기도 한다.

  • 멀티 쓰레드의 장점
    • CPU의 사용률을 향상 시킨다.
    • 자원을 보다 효율적으로 사용할 수 있다.
    • 사용자에 대한 응답성이 향상된다.
    • 작업이 분리되어 코드가 간결해진다.
  • 멀티 쓰레드 고려할 사항
    • 여러 쓰레드가 같은 프로세스 내에서 자원을 공유하면서 작업을 하기 때문에 동기화(synchronization), 교착상태(deadlock) 같은 문제들을 고려해서 프로그래밍 해야 한다.

쓰레드의 구현

  • Thread 클래스를 상속받는 방법
  • Runnable 인터페이스를 구현하는 방법

일반적으로 Runnable 인터페이스를 구현하는 방식을 사용함

  • 재사용성(reusability)를 높이고 코드의 일관성(consistency)를 유지할 수 있다.
public class ThreadEx1 {
    public static void main(String[] args) {
        ThreadEx1_1 t1 = new ThreadEx1_1();

        Runnable r = new ThreadEx1_2();
        Thread t2 = new Thread(r);

        t1.start();
        t2.start();
    }
}

class ThreadEx1_1 extends Thread {
    public void run() {
        for(int i = 0; i < 5; i++) {
            // Thread 클래스 메소드 직접 호출 가능
            System.out.println(getName());
        }
    }
}

class ThreadEx1_2 implements Runnable {
    @Override
    public void run() {
        for(int i = 0; i < 5; i++) {
            // Runnable을 구현하면 Thread클래스의 currentThread()를 호출하여 
            // 쓰레드에 대한 참조를 얻어와서 호출 해야 함
            System.out.println(Thread.currentThread().getName());
        }
    }
}

쓰레드의 실행

 t1.start() // 쓰레드 t1을 실행시킨다.
 t2.start() // 쓰레드 t2를 실행시킨다.
  • start()가 호출 되었다고 해서 바로 실행 되는 것이 아니라, 일단 실행 대기 상태에 있다가 자신의 차례가 되어야 실행상태가 된다.
  • 한가지 더 주의할 사항
    • 하나의 쓰레드에 대해 start()가 한번만 호출 될 수 있다.
    • 두번 실행 시 IllegalThreadStateException이 발생한다.
  • main 메서드에서 run()을 호출하는 것은 생성된 쓰레드를 실행 시키는 것이 아니라 단순히 클래스에 선언된 메서드를 호출하는 것일 뿐이다.

    실행 중인 사용자 쓰레드가 하나도 없을 때 프로그램은 종료 된다.
    -쓰레드의 종류: 사용자 쓰레드(user thread), 데몬 쓰레드(deamon thread)

싱글쓰레드와 멀티쓰레드

싱글 쓰레드와 멀티 쓰레드의 차이(싱글 코어)

  • 하나의 쓰레드로 두개의 작업을 하는 경우

    | A 작업 | B 작업 |

  • 두개의 쓰레드로 두개의 작업을 하는 경우

    |A|B|A|B|A|B|A|B|A|B|A|B|

싱글코어에서 단순히 CPU만을 사용하는 계산 작업이라면 오히려 멀티쓰레드보다 싱글쓰레드로 프로그래밍 하는 것이 더 효율적이다.

  • 이유: context switching: 쓰레드간 전환 작업(현재의 작업 상태를 저장하는 과정)

병행과 병렬

  • 싱글코어로 두개의 쓰레드를 실행 및 처리하는 경우: 병행(concurrent)
  • 멀티 코어로 두개의 쓰레드를 실행 및 처리하는 경우: 병렬(parallel)
    두 쓰레드가 서로 다른 자원을 사용하는 작업의 경우에는 싱글쓰레드 프로세스보다 멀티 쓰레드 프로세스가 더 효율적이다.
  • 예를 들면, 사용자로부터 데이터를 입력받는 작업, 네트워크로 파일을 주고받는 작업, 프린터로 파일을 출력하는 작업과 같이 외부기기와의 입출력이 필요로 하는 경우가 이에 해당된다.

쓰레드의 우선순위

쓰레드는 우선순위(priority)에 따라서 실행 시간이 달라진다.

  void setPriority(int newPriority) // Thread 우선순위를 지정한 값으로 변경한다.
  int getPriority() // Thread의 우선순위를 반환한다.

  public static final int MAX_PRIORITY = 10;
  public static final int MIN_PRIORITY = 1;
  public static final int NORM_PRIORITY = 5; // 보통 우선 순위
  • 싱글코어에서는 우선 순위의 영향을 받음
  • 멀티코어에서는 쓰레드의 우선순위를 더 주면 더 많은 실행 시간과 실행 기회를 갖는 것을 보장하지 못한다.
    • OS의 스케쥴링 정책과 JVM의 구현에 따라 다름
    • 작업에 우선 순위를 두어 PriorityQueue에 저장해놓고 그 우선순위가 높은 작업이 먼저 처리되도록 하는 것이 나을 수 있다.

쓰레드 그룹(thread group)

쓰레드 그룹은 보안상의 이유로 도입된 개념으로, 자신이 속한 쓰레드 그룹이나 하위 쓰레드 그룹은 변경할 수 있지만 다른 쓰레드 그룹의 쓰레드를 변경할 수 없다.

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);

java application을 실행하면 JVM은 main과 system이라는 쓰레드 그룹을 만든다.

  • main method를 수행하는 main이라는 쓰레드는 main 쓰레드 그룹에 속함
  • 가비지 컬렉션을 수행하는 finalizer쓰레드는 system 쓰레드에 속함

데몬 쓰레드(daemon thread)

데몬 쓰레드: 다른 일반 쓰레드(데몬 쓰레드가 아닌 쓰레드)의 작업을 돕는 보조적인 역할을 수행하는 쓰레드이다.

  • 데몬쓰레드는 모두 종료되면 데몬 쓰레드는 강제적으로 자동 종료 된다.
  • 이점을 제외하고는 일반 쓰레드와 차이가 없다.
  • 예를 들면, 가비지 컬렉터, 워드프로세서의 자동저장, 화면 자동 갱신 등이 있다.

데몬쓰레드는 무한 루프와 조건문을 이용해서 실행 후 대기하고 있다가 특정 조건이 만족되면 직업이 수행되고 다시 대기하도록 작성한다.

  boolean isDaemon() // 쓰레드가 데몬 쓰레드인지 확인한다.
  void setDaemon(boolean on) // 쓰레드를 데몬 쓰레드로 또는 사용자 쓰레드로 변경한다.
// Daemon thread example
public class ThreadEx10 implements Runnable {
    static boolean autoSave = false;

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

        for(int i = 1; i <= 10; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(i);
            if(i==5) {
                autoSave = true;                
            }
        }

        System.out.println("프로그램을 종료합니다.");
    }

    @Override
    public void run() {
        // **데몬쓰레드 실행을 위한 무한루프 실행 및 특정 조건 일때 실행**
        while(true) {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

            if(autoSave) {
                autoSave();
            }
        }
    }

    public void autoSave() {
        System.out.println("작업파일이 자동저장되었습니다.");
    }

}

쓰레드의 실행제어

Thread의 상태는 아래와 같다.

class ThreadEx15{
    public static void main(String args[]) {
        A th1 = new A();
        B th2 = new B();

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

        try {
            th1.sleep(5000);    
            // sleep(): 작업 흐름 대기시간 설정한다. 
            // 5초동안 대기시간 갖은 후에 다음 문자의 실행흐름을 이어 나간다.
        } catch(InterruptedException e) {}

        System.out.print("<<main 종료>>");
    } // main
}

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

class B extends Thread {
    public void run() {
        for(int i=0; i < 300; i++) {
            System.out.print("|");
        }
        System.out.print("<<th2 종료>>");
    } // run()
}
  • 결과
    • th1 종료 이 가장 늦게 종료 될 것으로 예상
    • th1 종료 -> th2종료 -> main 종료
    • 이유: sleep()이 항상 실행중인 쓰레드에 대해 작동하기 때문에 실제 영향을 받는 것은 main 메서드이기 때문
    • 그래서 sleep()은 static으로 선언되어 있으며, 참조 변수를 이용해서 실행하는 것보다 Thread.sleep(2000);과 같이 호출

interrupt()와 interrupted()
진행중인 쓰레드의 작업이 끝나기 전에 취소시켜야 할 때가 있는 경우 사용

  • 예를 들어 큰 파일을 다운로드 받을 때 시간이 너무 오래 걸리면 중간에 다운로드를 포기하고 취소할 수 있어야 한다.
  • interrupt()는 쓰레드에게 작업을 멈추라고 요청한다.
  • 실제 동작: 그저 쓰레드의 interrupted 상태(인스턴스 변수)를 바꾸는 것일 뿐이다.

Suspend(), resume(), sotp()

  • suspend(): sleep()처럼 쓰레드를 멈추게 한다.
  • resume(): suspend()에 의해 정지된 쓰레드는 resume()을 호출해야 다시 실행 대기 상태가 된다.
  • stop(): 호출되는 즉시 쓰레드가 종료된다.

쓰레드의 실행을 제어하는 가장 손쉬운 방법이지만, suspend()와 stop()이 교착상태(deadlock)을 일으키기 쉽게 작성되어 있으므로 사용이 권장 되지는 않는다. (deprecated)

yield()

  • yield(): 다른 쓰레드에게 양보한다.
  • 예를 들어 스케쥴러에 의해 1초 실행 시간을 할당받는 쓰레드가 0.5초의 시간동안 작업한 상태에서 yield를 호출되면, 나머지 0.5초는 포기하고 다시 실행 대기 상태가 된다.

yield()와 interrupt()를 적절히 사용하면 프로그램의 응답성을 높이고 보다 효율적인 실행이 가능하게 할 수 있다.

join()

  • join(): 쓰레드는 자신이 하던 작업을 멈추고 다른 쓰레드가 지정된 시간 동안 작업을 수행하도록 할 때 join()을 사용한다.
  • 참고:
    • join()도 sleep()처럼 interrupt()에 의해 대기 상태에서 벗어날 수 있다.
    • try-catch문으로 감싸야 한다.(InterruptedException)
    • sleep()과 다른 점은 join()은 특정 쓰레드에 대해 동작하므로 static 메서드가 아니라는 점이다.

0개의 댓글

관련 채용 정보