JAVA/about java

[thread] 멀티 스레드와 작업 스레드 생성 방법

dev_rosieposie 2023. 8. 31. 19:33

Goal

  1. multi thread에 대해 알아본다
  2. 작업스레드의 생성과 실행을 해본다
    1. Thread 클래스로부터 직접 생성
    2. Thread 하위 클래스로부터 생성
    3. Thread 이름

 multi-thread 란  ?

프로세스와 스레드

  1. 프로세스란, 운영체제에서 실행 중인 하나의 어플리케이션을 말한다. 사용자가 어플리케이션을 실행하면 운영체제로부터 실행에 필요한 메모리를 할당받아 어플리케이션의 코드를 실행한다.
    • 하나의 어플리케이션은 다중 프로세스를 만들기도 한다 ex) 크롬 브라우저 두개 실행 - 크롬 프로세스 2개 생성
    • 멀티 프로세스가 어플리케이션 단위의 멀티 태스킹
    • 운영체제로 부터 할당 받은 자신의 메모리를 사용하기에, 서로 독립적이다. 
    • 즉, 하나의 프로세스에서 오류가 발생해도 다른 프로세스에 영향을 끼치지 않는다.
  2. 멀티태스킹이란, 두 가지 이상의 작업을 동시에 처리하는 것이다.
    • 운영체제는 멀티 태스킹을 할 수 있도록 cpu 및 메모리 자원을 프로세스마다 적절히 할당해주고, 병렬로 실행시킨다.
    • 꼭 멀티 태스킹이 멀티 프로세스를 뜻하는 것이 아니다.
    • 한 프로세스 내에서 멀티 태스킹이 가능 ex) 미디어 플레이어(동영상 재생과 음악 재생 두 작업 동시처리)와 메신저(채팅 제공 동시에 파일 전송 기능)
    • 하나의 프로세스가 두 가지 이상의 작업을 할 수 있는 이유 => 멀티 스레드 
  3. 스레드란, 한 가지 작업을 실행하기 위해 순차적으로 실행할 코드를 실처럼 이어 놓음 = 하나의 스레드는 하나의 코드 실행 흐름
    • 한 프로세스 내 스레드가 두 개라면 두 개의 코드 실행 흐름이 생김
    • 멀티 스레드는 하나의 프로세스 내부에 생성되기 때문에 하나의 스레드가 예외를 발생시키면 프로세스 자체가 종료될 수 있어, 다른 스레드에 영향을 미친다.
    • 대용량 데이터의 처리 시간을 줄이기 위해 데이터를 분할하여 병렬로 처리네트워크 통신 또는 다수의 클라이언트 요청 처리 서버를 개발할 때 사용된다.

멀티 프로세스

메인 스레드

모든 자바 어플리케이션은 메인 스레드가 main() 메소드를 실행하면서 시작된다. 메인 스레드는 main() 메소드의 첫 코드부터 순차적으로 실행하고 마지막 코드를 실행하거나 return문을 만나면 종료된다.

싱글스레드와 멀티스레드

  • 메인 스레드는 필요에 따라 작업 스레드를 만들어 병렬로 코드 실행이 가능하다. 즉, 멀티 스레드를 생성해서 멀티 태스킹을 수행한다
  • 싱글 스레드 어플리케이션에서는 메인 스레드가 종료하면 프로세스도 종료되지만, 멀티 스레드 어플리케이션에서는 실행 중인 스레드가 하나라도 있으면 프로세스는 종료되지 않는다.
  • 메인 스레드가 작업 스레드보다 먼저 종료되더라도 작업 스레드가 실행 중이면 프로세스는 종료되지 않는다.

 작업 스레드 생성과 실행 

메인스레드는 자바 어플리케이션에 반드시 존재하며, 메인 작업 이외에 추가적인 병렬 작업의 수만큼 스레드를 생성하면 된다.

자바에서는 작업 스레드도 객체로 생성되기 때문에 클래스가 필요하다. 

  1. java.util.Thread 클래스를 직접 객체화 생성
  2. Thread를 상속해서 하위 클래스를 만들어 생성

Thread 클래스로부터 직접 생성

java.util.Thread 클래스로부터 작업 스레드 객체를 직접 생성하려면 Runnable을 매개값으로 갖는 생성자를 호출해야한다.

Thread thread = new Thread(Runnable target);

Runnable이란?

인터페이스 타입으로 구현 객체를 만들어 대입해야한다. run()메소드가 정의되어 있고, 구현 클래스는 run()을 재정의하여 작업 스레드가 실행할 코드를 작성해야 한다.

public class Task implements Runnable{

    public void run() {
        // 스레드가 실행할 코드
    }
}
Runnable task = new Task();
Thread thread = new Thread(task);
  • Runnable은 작업 내용을 가지고 있는 객체이지 실제 스레드는 아니므로, Runnable 구현 객체를 생성하고 매개값으로 Thread 생성자를 호출하면 작업 스레드가 생성된다.
Thread thread = new Thread(new Runnable() {
    public void run() {
        // 스레드가 실행할 코드
    }
});
  • 위와 같이 코드를 절약하기 위해 Thread 생성자 호출시 Runnable 익명 객체를 매개값으로 사용 가능
Thread thread = new Thread(()->{
    // 스레드가 실행할 코드
});
  • Runnable 인터페이스는 run() 메소드 하나만 정의되어 있어 함수적 인터페이스이므로, 람다식을 매개값으로 사용 가능
thread.start();
  • 작업 스레드는 즉시 실행되는 것이 아니라, start() 메소드를 호출해야 실행된다.

 

 

0.5초 주기로 비프음을 발생시키면서 동시에 프린팅하는 작업이 있다고 가정해보자.

메인 스레드만 이용한 경우

public class BeefPrintExample {

    public static void main(String[] args){
        Toolkit toolkit = Toolkit.getDefaultToolkit();

        for(int i=0; i<5; i++){
            // 비프음 발생
            toolkit.beep();
            try{
                // 0.5초간 일시정지
                Thread.sleep(500);
            }catch (Exception e){}
        }

        for(int i=0; i<5; i++){
            System.out.println("띵");
            try{
                // 0.5초간 일시정지
                Thread.sleep(500);
            }catch (Exception e){}
        }
    }
}

결과





문제 

비프음 발생과 프린팅은 서로 다른 작업이므로, 메인 스레드가 동시에 두 작업을 처리할 수 없다. 위의 코드는 메인 스레드는 비프음을 모두 발생한 다음, 프린팅을 시작한다.

 

메인 스레드와 작업 스레드가 동시 실행한 경우

public class BeefTask implements Runnable{

    public void run() {
        Toolkit toolkit = Toolkit.getDefaultToolkit();
        for(int i=0; i<5; i++){
            try{
                // 0.5초간 일시정지
                Thread.sleep(500);
            }catch (Exception e){}
        }
    }
}

비프음을 들려주는 작업 스레드를 정의

public class BeefPrintExample2 {

    // 메인 스레드만 이용하는 경우
    // 0.5초 주기로 비프음을 발생시키면서 동시에 프린팅하는 작업이 있다고 가정
    public static void main(String[] args){

        /*
        1. 첫번째 방법
        Runnable beefTask = new BeefTask();
        Thread thread = new Thread(beefTask);
        thread.start();*/

        /*2. 두번째 방법
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                Toolkit toolkit = Toolkit.getDefaultToolkit();
                for(int i=0; i<5; i++){
                    try{
                        // 0.5초간 일시정지
                        Thread.sleep(500);
                    }catch (Exception e){}
                }
            }
        });*/

        // 3. 세번째 방법 - 람다식 사용
        Thread thread = new Thread(()->{
            Toolkit toolkit = Toolkit.getDefaultToolkit();
            for(int i=0; i<5; i++){
                try{
                    // 0.5초간 일시정지
                    Thread.sleep(500);
                }catch (Exception e){}
            }
        });

        for(int i=0; i<5; i++){
            System.out.println("띵");
            try{
                // 0.5초간 일시정지
                Thread.sleep(500);
            }catch (Exception e){}
        }
    }
}

결과





 

Thread 하위 클래스로부터 생성

작업 스레드가 실행할 작업을 Runnable이 아닌, Thread의 하위 클래스로 작업 스레드를 정의하는 방법이다.

Thread 클래스를 상속한 후 run 메소드를 재정의 해서 스레드 실행 코드를 작성하면 된다.

public class BeefThread extends Thread{
    @Override
    public void run() {
        Toolkit toolkit = Toolkit.getDefaultToolkit();
        for(int i=0; i<5; i++){
            try{
                // 0.5초간 일시정지
                Thread.sleep(500);
            }catch (Exception e){}
        }
    }
}

 

메인 스레드와 작업 스레드가 동시 실행

public class BeefPrintExample3 {

    // 메인 스레드만 이용하는 경우
    // 0.5초 주기로 비프음을 발생시키면서 동시에 프린팅하는 작업이 있다고 가정
    public static void main(String[] args){

        /* 1.첫번째 방법
        Thread thread = new BeefThread();
         */

        /* 2.두번째 방법 */
        Thread thread = new Thread(){
            @Override
            public void run() {
                Toolkit toolkit = Toolkit.getDefaultToolkit();
                for(int i=0; i<5; i++){
                    try{
                        // 0.5초간 일시정지
                        Thread.sleep(500);
                    }catch (Exception e){}
                }
            }
        };
        thread.start();

        for(int i=0; i<5; i++){
            System.out.println("띵");
            try{
                // 0.5초간 일시정지
                Thread.sleep(500);
            }catch (Exception e){}
        }
    }
}

결과





 

스레드의 이름

스레드는 자신의 이름을 가지고 있는데, 메인스레드는 main의 이름을, 우리가 직접 생성한 스레드는 자동적으로 "Thread-n"이라는 이름으로 설정된다. n은 스레드의 번호를 말하며, setName()메소드로 이름을 변경할 수 있다.

public class ThreadA extends Thread{
    public ThreadA() {
        setName("ThreadA");
    }

    @Override
    public void run() {
        for(int i=0; i<2; i++){
            System.out.println(getName() +"가 출력한 내용");
        }
    }
}
public class ThreadB extends Thread{
    public ThreadB() {
        setName("ThreadB");
    }

    @Override
    public void run() {
        for(int i=0; i<2; i++){
            System.out.println(getName() +"가 출력한 내용");
        }
    }
}
public class ThreadNameExample {
    public static void main(String[] args){
        Thread mainThread = Thread.currentThread();
        System.out.println("프로그램 시작 스레드 이름 : "+mainThread.getName());

        Thread threadA = new ThreadA();
        System.out.println("작업 스레드 이름 : "+ threadA.getName());
        threadA.start();

        Thread threadB = new ThreadB();
        System.out.println("작업 스레드 이름 : "+ threadB.getName());
        threadB.start();
    }
}

결과

프로그램 시작 스레드 이름 : main
작업 스레드 이름 : ThreadA
작업 스레드 이름 : ThreadB
ThreadA가 출력한 내용
ThreadA가 출력한 내용
ThreadB가 출력한 내용
ThreadB가 출력한 내용

 

 

 

 

참고

이것이 자바다 by 신용권