본문 바로가기
[ JAVA ]/JAVA

[ Java ] Thread

by 환이s 2023. 1. 5.
CHAPTER 19. Thread 알아가기

오늘은 Java에서 특정한 Task를 돌릴 때 동시에 여러 일을 수행할 수 있게 해주는 Thread에 대해 포스팅해보려 합니다!

 

1 ) 프로세스(Process)와 스레드(Thread)

 

  • 프로세스(Process) : 실행 중인 프로그램(운영체제로부터 자원을 할당받는 작업의 단위)라고 할 수 있습니다.

즉, 사용자가 작성한 프로그램이 운영체제에 의해 메모리 공간을 할당받아 실행 중인 것을 말합니다.

  • 스레드(Thread) : 프로세스 내에서 할당받은 자원을 이용하는 실행의 단위입니다. 모든 프로세스에는 한 개 이상의 스레드가 존재하여 작업을 수행하며, 두 개 이상의 스레드를 가지는 프로세스를 멀티스레드 프로세스(multi-threaded process)라고 합니다.

 

예를 들면 운영체제(OS)에서 실행 중인 하나의 애플리케이션 즉, 작업 관리자에서 프로세스 탭에 올라와 있는 애플리케이션 하나를 하나의 프로세스라고 부르는데, 만약 우리가 크롬을 2개 띄웄다면 두 개의 프로세스가 생성된 것입니다.

 

 

2 ) 멀티 태스킹(Multi Tasking)과 멀티 스레드(Multi Thread)

 

 멀티 태스킹이란 두 가지 이상의 작업을 동시에 처리하는 것을 말합니다. 예를 들어 워드로 문서작업을 하는 동시에 음악을 듣는 것은 OS가 프로세스마다 작업을 병렬로 처리하기에 가능합니다.

 

또한  멀티 태스킹이 꼭 멀티 프로세스(워드 + 곰플레이어 프로세스 조합)를 말하는 것은 아닌데, 한 프로세스 내에서도 멀티 태스킹을 할 수 있도록 만들어진 예를 보면 카톡, 메신저 프로세스 같은 경우 채팅 기능을 제공하면서 동시에 파일 업로드 기능을 수행할 수 있습니다.

이처럼 한 프로세스에서  멀티 태스킹이 가능한 이유는 멀티 스레드 덕분이라고 보시면 됩니다.

 

멀티 프로세스는 프로세스마다 운영체제로부터 할당받은 고유의 메모리를 서로 침범할 수 없지만 멀티 스레드는 프로세스 내부에서의 스레드들끼리 공유되는 자원이 있어서 하나의 스레드에서 예외가 발생한다면 프로레스 자체가 종료될 수 있습니다.

 

 

2-1 ) Multi Thread를 사용해야 하는 경우

 

  • GUI 프로그래밍에서는 main Thread에서만 UI를 그리거나 갱신할 수 있습니다.
  • 시간이 오래 걸리는 작업의 경우 ANR(Application not Responding) 현상을 방지하기 위해 백 그라운드에서 실행되는 별도의 Thread가 필요합니다.
  • 만약 그렇지 않으면 화면을 갱신하고자 하는 모든 코드는 block 당하여 ANR(Application not Responding)이 발생하게 됩니다.

 

2-2 ) Multi Thread의 장단점

 

  • 장점

- 자원을 보다 효율적으로 사용할 수 있습니다.

- 사용자에 대한 응답성(responseness)이 향상됩니다.

- 작업이 분리되어 코드가 간결해 집니다.

 

  • 단점

- 동기화(synchronization)에 주의해야 합니다.

- 교착상태(dead-lock)가 발생하지 않도록 주의해야 합니다.

- 각 Thread가 효율적으로 고르게 실행될 수 있게 해야 합니다.

"프로그래밍할 때 고려해야 할 사항들이 많습니다."

 

 

3 ) Thread의 상태제어

 

 

위 사진처럼 Thread가 3개가 있다면 JVM은 시간을 잘게 쪼갠 후에 한 번은 Thread1을, 한 번은 Thread2를, 한번은 Thread3을 실행합니다. 이것이 빠르게 일어나다 보니 Thread가 모두 동작하는 것처럼 보입니다.

 

 

4 ) Thread 구조

 

  • Thread는 실행가능 상태인 Runnable과 실행 상태인 Running 상태로 나뉩니다.
  • 실행되는 Thread 안에서 Thread.sleep()이나 Object가 가지고 있는 wait() 메서드가 호출이 되면 Thread는 블록 상태가 됩니다.
  • Thread.sleep()은 특정시간이 지나면 자신 스스로 블록상태에서 빠져나와 Runnable이나 Running 상태가 됩니다.
  • Object가 가지고 있는 wait() 메서드는 다른 Thread가 notify()나 notifyAll() 메서드를 호출하기 전에는 블록상태에서 해제되지 않습니다. (그래서 대기 중인 다른 메서드가 실행하게 됩니다.)
  • Thread의 run 메서드가 종료되면, Thread는 종료됩니다. 
  • Thread의 yeild() 메서드가 호출되면 해당 Thread는 다른 Thread에게 자원을 양보하게 됩니다.
  • Thread가 가지고 있는 join() 메서드를 호출하게 되면 해당 Thread가 종료될 때까지 대기하게 됩니다.

 

Thread 구조

 

5  ) Thread 구현과 실행

 

자바에서 Thread를 생성하는 방법에는 다음과 같이 두 가지 방법이 있습니다.

 

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

 

두 방법 모두 스레드를 통해 작업하고 싶은 내용을 run() 메서드에 작성하면 됩니다.

 

다음 예제는 위의 두 가지 방법 중 스레드를 생성하고 실행하는 예제입니다.

//멀티스레드 : 작업단위가 2개 이상
//멀티스레드 구현하는 방법
//1. Thread를 상속

public class ThreadExam extends Thread{
    public ThreadExam(String name){
        super(name);//부모생성자 호출 - 타이틀 설정
    }

    @Override // 반드시 오버라이드
    public void run() {// 스레드 실행 메소드
        for (int i=1; i<=5; i++){
            System.out.println(Thread.currentThread().getName());
            try {
                Thread.sleep(1000); // Cpu를 1초간 멈춤
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public static void main(String[] args) {
        ThreadExam e1 = new ThreadExam("thread1");
        ThreadExam e2 = new ThreadExam("thread2");
        ThreadExam e3 = new ThreadExam("thread3");

        //e1.run()을 호출하면 main Thread가 호출됨
        e1.start();// 스레드객체.start() =>run()이 호출됨
        e2.start();// 위와 동시에 호출
        e3.start();// 위와 동시에 호출

    }
}

 

위 예제의 실행 결과를 살펴보면, 생성된 Thread가 서로 번갈아가며 실행되고 있는 것을 확인할 수 있습니다. 

또한, 순서대로 출력이 안되며, cpu가 마음대로 출력해서 보내는 걸 알 수 있는데 Thread는 자바에서 우선순위를 관여해서 자신만의 필드를 생성할 수 있습니다.

 

아래 예제는 Runnable 인터페이스를 구현하는 방법입니다.

 

public class RunnableExam implements Runnable{
    @Override
    public void run() {
        for (int i=1; i<=100; i++){
            System.out.println(Thread.currentThread().getName()+"==>"+i);
        }
    }// end run()

    public static void main(String[] args) {
        RunnableExam e1 = new RunnableExam();

        // Runnabla을 쓸 때는 Thread를 별도로 생성해서 써야한다.
        // Java는 단일 상속만을 하기 때문에 다른 객체와 함께 상속받아 스레드를 구현하려면
        // implements Runnable로 처리
        // new Thread(스레드구현객체, "스레드이름")
        Thread t1 = new Thread(e1, "스레드1");
        Thread t2 = new Thread(e1, "스레드2");

        t1.start();//run()호출
        t2.start();

        // t1.run()을 하면 main인 싱글스레드가 돌아간다.
//        t1.run();
//        t2.run();
    }
}

 

위 예제들을 실행 결과를 살펴보면, Thread 클래스를 상속받으면 다른 클래스를 상속받을 수 없으므로, 일반적으로 Runnable 인터페이스를 구현하는 방법으로 Thread를 생성합니다.

 

※ Runnable 인터페이스는 몸체가 없는 메서드인 run() 메서드 단 하나만을 가지는 간단한 인터페이스입니다.

 

6 ) Thread의 우선순위

 

위 예제들처럼 cpu가 마음대로 출력하기 때문에 자바에서는 각 Thread에 우선순위(priority)에 관한 자신만의 필드를 가지고 있습니다.

이러한 우선순위에 따라 특정 스레드가 더 많은 시간 동안 작업을 할 수 있도록 설정할 수 있습니다.

 

필 드 설 명
NORM_PRIORITY Thread가 생성될 때 가지는 기본 우선순위를 명시합니다.
MIN_PRIORITY Thread가 가질 수 있는 최소 우선순위를 명시합니다.
MAX_PRIORITY Thread가 가질 수 있는 최대 우선순위를 명시합니다.

getPriority(), setPriority() setPriority() 메서드를 통해 Thread의 우선순위를 반환하거나 변경할 수 있습니다.

Thread의 우선순위가 가질 수 있는 범위는 1~10까지 이며, 숫자가 높을수록 우선순위 또한 높아집니다.

 

하지만 우선순위는 비례적인 절댓값이 아닌 어디까지나 상대적인 값일 뿐입니다. 

 

우선순위가 10인 Thread가 우선순위가 1인 Thread 보다 더 빨리 수행되는 것은 아닙니다. 단지 우선순위가 10인 Thread는 우선순위가 1인 Thread보다 좀 더 많이 실행 큐에 포함되어, 좀 더 많은 작업 시간을 할당받을 뿐입니다.

 

 

다음 예제로 getPriority() , setPriority()setPriority() 메서드를 통해 Thread의 우선순위를 알아봅시다.

 

public class Priority extends Thread{
    @Override
    public void run() {
    for (int i=1; i<=10; i++){
        System.out.println(Thread.currentThread().getName()+"==>"+i);
    }
    }

    public static void main(String[] args) {
        Priority e1 = new Priority();
        Priority e2 = new Priority();
        //스레드 이름 설정

        e1.setName("스레드1");
        e2.setName("스레드2");
        System.out.println("e1의 기본 우선순위 : "+ e1.getPriority());
        System.out.println("e2의 기본 우선순위 : "+ e2.getPriority());

        //e1.setPriority(7);//숫자로도 줄 수 있으나 상수값으로 해야 그나마 cpu에 잘 반영됨
        e1.setPriority(Thread.MIN_PRIORITY);//최소 우선순위(1)
        e2.setPriority(Thread.MAX_PRIORITY);//최대 우선순위(10)
        e1.start();
        e2.start();

    }
}

 

위 예제 실행 결과를 보면 우선순위를 먼저 정한 스레드 2가 다 끝나고 실행되는 게 아닌, 좀 더 실행큐에 포함되어, 좀 더 많은 작업시간을 할당받는 걸 알 수 있습니다.

 

 

7 ) synchronized

 

 synchronized 키워드는 한 번에 하나의 스레드만 객체에 접근할 수 있도록 객체에 락(lock)을 걸어서 데이터의 일관성을 유지할 수 있게 해 줍니다.

 

그럼 예제 코드를 통해서 알아봅시다. Thread를 활용하여 MusicBox 플레이를 해보았습니다.

public class MusicBox { // 공유객체
    //모니터링 락(객체의 사용권)
    public synchronized void playMusicA(){


        for (int i=0; i<10; i++){
            System.out.println("가요 음악!!!");
            try {
                Thread.sleep((int)(Math.random()*1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public synchronized void playMusicB(){
        for (int i=0; i<10; i++){
            System.out.println("pop 음악!!!");
            try {
                Thread.sleep((int)(Math.random()*1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public void playMusicC(){
        for (int i=0; i<10; i++){
            //메소드의 코드가 길어지면, 마지막에 대기하는 스레드가 너무 오래 기다리는 것을 막기 위해서
            //메소드 헤드에 synchronized를 처리 안하고 필요한 부분만 synchronized 블록을 사용할 수 있다.
            synchronized (this){//(this)는 MusicBox객체 자신을 가리킴
            System.out.println("클래식 음악!!!");
            }
            try {
                Thread.sleep((int)(Math.random()*1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}
public class MusicPlayer extends Thread {
    int type;
    MusicBox musicBox;

    public MusicPlayer(int type, MusicBox musicBox){
        this.type = type;
        this.musicBox = musicBox;
    }

    @Override
    public void run() {
        switch (type){
            case 1: musicBox.playMusicA();break;
            case 2: musicBox.playMusicB();break;
            case 3: musicBox.playMusicC();break;
        }
    }
}
public class MusicBoxExam1 {
    public static void main(String[] args) {
        MusicBox box = new MusicBox();

        MusicPlayer kim = new MusicPlayer(1,box);
        MusicPlayer lee = new MusicPlayer(2,box);
        MusicPlayer park = new MusicPlayer(3,box);

        // 스레드 실행
        kim.start();
        lee.start();
        park.start();
    }
}

위 예제 코드를 실행결과 한 스레드가 수행할 때 다른 스레드에 의해 간섭을 받이 않는 걸 알 수 있습니다.

 

8 ) 데몬 스레드(Daemon Thread)

 

데몬 스레드는 메인 스레드의 작업을 돋는 보조적인 역할을 수행하는 스레드입니다. 주 스레드가 종료되면 데몬 스레드는 더는 존재 의미가 없기에 강제로 종료시킵니다. 워드의 자동 저장 기능을 예로 들을 수 있는데, 데몬 스레드를 만드는 방법은 스레드를 만들고 해당 스레드에 setDaemon(true); 메서드를 세팅합니다.

 

그럼 예제 코드로 알아봅시다.

public class DaemonThread implements Runnable{
    @Override
    public void run() {
        while (true){ // while(true)지만 main 스레드가 종료되면 자동 종료됨.
            System.out.println("데몬 스레드가 실행중입니다.");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
                break;//Exception 발생시 while문 빠져나가도록
            }
        }//while문
    }//run

    public static void main(String[] args) {
        Thread th = new Thread(new DaemonThread());
        th.setDaemon(true);//데몬스레드 설정
        th.start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("메인 스레드가 종료됩니다.");
    }
}

 

위 예제 출력결과를 보면, while(true)지만 , main 스레드가 종료되면서 자동으로 종료되는 걸 알 수 있습니다.


마치며

 

기본적으로 Thread 구현하는 방법 및 개념을 정리해 보았습니다. 만약에 자원을 동시에 사용하거나 순서의 제어가 필요한 경우에는 동시성 처리를 해줘야 할 필요가 있습니다!! 

728x90

'[ JAVA ] > JAVA' 카테고리의 다른 글

[ Java ] GUI 프로그래밍 응용  (2) 2023.01.09
[ Java ] GUI 프로그래밍 개념  (2) 2023.01.07
[ Java ] 예외처리(Exception Handling)  (0) 2023.01.04
[ Java ] 내부 클래스(Inner Class)  (0) 2023.01.03
[ Java ] Scanner  (0) 2022.12.29