쓰레드(Thread)

KDG: First things first!·2024년 7월 31일
0

CS

목록 보기
1/6

프로세스(Process)와 쓰레드(Thread)란?


프로세스: 운영체제로부터 자원을 할당받은 작업의 단위 또는 단순히 실행 중인 하나의 프로그램이라고 할 수 있다.


쓰레드: '프로세스 내에서 실행되는 기본 작업의 단위' 또는 '프로세스 내에서 실제 작업을 수행하는 각 주체'이다.



프로세스의 구조


OS가 프로그램 실행을 위한 프로세스를 할당할 때에는 그 프로세스 안에 프로그램 관련 'Code' 'Data' 그리고 'Memory'(Stack, Heap)를 같이 할당해준다.

Data: 프로그램이 실행 중 저장할 수 있는 저장공간이다. (전역변수, 정적변수, 배열 등 초기화된 데이터를 저장하는 공간)

Memory: 지역 변수, 참조 변수, 매개 변수 등의 리턴 변수가 저장되는 Stack과 프로그램에 동적으로 필요한 변수가 저장되는 공간인 Heap이 있다.





쓰레드의 생성과 자원

▶︎ 쓰레드 생성: 작업 중인 프로세스(프로그램)에서 실행요청이 들어오면 쓰레드를 만들어 해당 요청을 실행한다.

▶︎ 쓰레드 자원: 프로세스 안에는 여러 쓰레드들이 존재하며 쓰레드들은 작업 실행에 필요한 주소공간과 HEAP를 공유한다. 또한 쓰레드들은 각각 작업실행을 위해 자신만의 Stack을 각각 할당받는다.



Java 프로그램을 실행하면 기본적으로 JVM 프로세스에서 실행되며 이 때 기본적으로 Java Main 쓰레드도 JVM 프로세스와 같이 실행된다. 메인 쓰레드가 종료되면 JVM도 같이 종료된다.




싱글 쓰레드 vs 멀티 쓰레드

Java는 메인(Main) 쓰레드가 병렬로 코드를 실행시킬 수 있어 필요할 때 쓰레드들을 여러 개 생성하여 작업할 수 있다.


싱글 쓰레드: 프로세스 안에서 실행되는 쓰레드가 하나인 경우

(JVM 프로세스 실행 중인데 쓰레드가 메인 쓰레드 1개인 경우가 이에 해당한다.)


멀티 쓰레드: 프로세스 안에서 여러 개의 쓰레드가 동시에 실행되는 경우

(메인 쓰레드가 본인 외에 다른 작업 쓰레드들을 생성하여 여러 개의 작업을 동시에 실행하여 여러 실행흐름을 만든다.)




멀티 쓰레드의 장단점

장점:

▶︎ 여러 개의 작업을 동시에 진행할 수 있어 성능이 향상된다.

▶︎ 프로세스가 가지고 있는 메모리 등의 모든 영역(Stack 제외)들을 공유해서 사용하기
때문에 자원 사용에 있어 효율적이다.

▶︎응답 쓰레드와 작업 쓰레드를 분리하여 빠르게 응답을 줄 수 있다(비동기화).

단점:

▶︎ 프로세스의 자원을 쓰레드들이 공유하면서 사용하기 때문에 자원을 서로 하용하려고 하면 쓰레드간 충돌이 일어날 수 있다.(동기화 문제)

▶︎ 복수의 쓰레드가 서로가 사용하고 있는 자원을 서로 원하여 자원을 가져오기 위해 서로 상대 쓰레드가 작업을 종료할 때까지 무기한 대기하는 교착 상태를 의미하는 '데드락'이 발생할 수 있다.

하지만 이러한 단점들을 극복하고 멀티 쓰레드들을 효율적으로 사용할 수 있는 기법들이 존재한다.




(Java) 쓰레드 구현 방법

1. Thread 클래스를 상속하여 쓰레드 구현

public class MyThread extends Thread {

    @Override
    public void run() { // run(): 쓰레드에 해당 작업을 명령
        // 실제 쓰레드에서 수행할 작업 코드
        System.out.println("Thread 클래스 상속을 통한 쓰레드 구현 확인");
    }
}

자바 내에 구현되어 있는 Thread 클래스를 상속받아 쓰레드에 해당 작업을 명령하는 메서드인 run()을 오버라이딩하여

class Main {

    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    }
}

// 출력 값: Thread 클래스 상속을 통한 쓰레드 구현 확인

메인 메서드에서 쓰레드 객체를 생성한 후 쓰레드를 가동시키는 Thread 클래스의 내장 메서드 start()를 호출하면 쓰레드가 명령을 실행한다.



2. Runnable 인터페이스를 통한 쓰레드 구현

public class RunnableThread implements Runnable{

    @Override
    public void run() {
        // 쓰레드에 지시할 작업 정의
        System.out.println("Runnable 인터페이스를 통한 쓰레드 구현 확인");
    }
}

쓰레드를 구현과 가동에 사용되는 Runnable 인터페이스를 implements하고 쓰레드에 지시할 작업을 정의하는 run() 메서드를 오버라이딩한 후


class Main {

    public static void main(String[] args) {
        Runnable run = new RunnableThread();
        Thread thread = new Thread(run);
        
        thread.start();
    }
}
// Runnable 인터페이스를 통한 쓰레드 구현 확인

Runnable 객체와 쓰레드 객체를 각각 만들고 쓰레드 객체에 생성자 파라미터로 Runnable 객체 변수를 넘겨주면 쓰레드를 사용할 준비가 완료된다. Thread 클래스를 직접 상속하여 쓰레드를 구현하는 것보다 더 흔하게 사용되는 방법이다.



3. 람다식을 이용한 쓰레드 구현

public class Main {
    public static void main(String[] args) {
      
      // 람다식을 사용하여 Runnable 구현
        Runnable task = () -> {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + " - Count: " + i);

            }
        };

        // Runnable 객체를 이용해 쓰레드 생성
        Thread thread1 = new Thread(task);
        thread1.setName("thread1");
        Thread thread2 = new Thread(task);
        thread1.setName("thread2");
        
        // 쓰레드 실행
        thread1.start();
        thread2.start();
    }

}



// 출력값:
Thread-1 - Count: 0
thread2 - Count: 0
Thread-1 - Count: 1
thread2 - Count: 1
Thread-1 - Count: 2
thread2 - Count: 2
Thread-1 - Count: 3
thread2 - Count: 3
Thread-1 - Count: 4
thread2 - Count: 4

이전 방법들에서 run() 메서드의 블록 안에 적었던 내용들을 람다식을 이용하여 Runnable task 변수 안에 담는다. 이후 2번째 방법과 마찬가지로 쓰레드 객체를 생성할 때 Runnable 객체의 참조 변수인 task 변수를 생성자 인자로 넘겨주고 start()로 쓰레드들을 실행하면 된다.

setName()으로 쓰레드들에 이름을 할당할 수 있다.

쓰레드 여러 개가 병렬적으로 수행되기 때문에 순서가 전혀 보장되지 않아 실행 때마다 출력값이 다르게 나온다.

람다식을 이용한 쓰레드 구현은 매우 자주 사용되는 방식이기 때문에 이해해두는 것을 추천한다.

Thread.currentThread().getName() # 현재 실행 중인 쓰레드 이름 출력



데몬 쓰레드 vs 사용자 쓰레드

데몬 쓰레드

데몬 쓰레드: 백그라운드에서 실행되는 낮은 우선순위의 쓰레드이다.
보조적인 역할들을 수행하면 대표적인 데몬 쓰레드로는 메모리 영역을 주기적으로 정리해주는 가비지 컬렉터가 존재한다. 일반적인 쓰레드들에 비해 리소스를 적게 할당받는다.

public class Main {
    public static void main(String[] args) {

        // 데몬 쓰레드 구현
        Runnable demon = () -> {
            for (int i = 0; i < 10000; i++) {
                System.out.println("데몬 쓰레드 구현");
            }
        };

        Thread thread = new Thread(demon);
        thread.setDaemon(true); // 생성한 쓰레드를 데몬 쓰레드로 설정

        thread.start(); 

        // 메인 쓰레드의 작업 -> 끝나면 데몬 쓰레드 작업 종료 여부 관계없이 JVM 종료
        for (int i = 0; i < 100; i++) {
            System.out.println(i);
        }
    }
}

데몬 쓰레드를 만드는 방법은 일반 쓰레드를 만드는 방법과 동일하다.

[thread 객체 이름].setDaemon(true)

다만 위의 코드를 실행하면 생성된 일반 쓰레드는 데몬 쓰레드로 설정된다.

또한 데몬 쓰레드는 다른 일반적인 쓰레드보다 더 우선순위가 낮기 때문에 만약 데몬 쓰레드가 아직 작업이 진행 중이었다고 해도 메인 쓰레드의 작업이 끝나면 데몬 쓰레드의 작업 종료를 기다리지 않고 그대로 JVM이 종료된다는 특징이 존재한다.



사용자 쓰레드

사용자 쓰레드: 보이는 곳(foreground)에서 실행되는 높은 우선순위를 가진 쓰레드이다.
메인 쓰레드를 포함하여 직접 생성한 쓰레드들은 setDaemon() 메서드를 통해 데몬 쓰레드로 따로 설정하지 않는 이상은 모두 기본적으로 사용자 쓰레드이다.

앞서 말했듯이 JVM은 사용자 쓰레드의 작업이 모두 끝나면 데몬 쓰레드의 작업 종류 여부와 상관없이 데몬 쓰레드들도 전부 종료시켜버린다.




쓰레드 우선순위

쓰레드마다 중요도가 달라 중요한 쓰레드들은 더 많은 리소스를 할당하여 더 빠르게 처리하고 싶을 때 쓰레드에 우선순위를 부여하는 것이 가능하다.

이 우선순위는 사용자가 직접 지정하거나 JVM에 의해 자동 지정될 수 있다.

우선순위는 다음과 같이 3가지 우선순위로 나뉘어진다.

  • 최대 우선순위(MAX_PRIORITY) : 10
  • 최소 우선순위(MIN_PRIORITY) : 1
  • 최대 우선순위(NORM_PRIORITY) : 5

스레드의 우선순위가 가질 수 있는 범위는 JVM에서 설정한 1부터 10까지이며, 숫자가 높을수록 우선순위 또한 높아진다.

우선순위를 지정하지 않는다면 기본값은 보통 우선순위(5)이다.

[쓰레드 변수명].getPriority() // 해당 쓰레드 우선순위 반환(확인)

[쓰레드 변수명].setPriority(n) // 해당 쓰레드 우선순위 설정

getPriority()와 setPriority() 메소드를 통해 스레드의 우선순위를 반환하거나 변경할 수 있다.




쓰레드 그룹

쓰레드 그룹: 복수의 쓰레드들을 하나의 그룹으로 묶어서 다루는 것이다.
모든 쓰레드들은 반드시 하나의 그룹에 포함되어야 한다. 위에서 예시로 만들었던 쓰레드들처럼 따로 쓰레드 그룹을 지정하지 않으면 자동으로 main 그룹에 포함된다.

// ThreadGroup 클래스로 쓰레드 그룹 객체 만들기
        ThreadGroup threadGroup1 = new ThreadGroup("ThreadGroup1");

        // 쓰레드 그룹 지정하려면 쓰레드 객체 생성시 첫 번째 파라미터로 쓰레드 그룹 정의
        // Thread(ThreadGroup group, Runnable task, String name) 형식
        Thread thread1 = new Thread(threadGroup1, task, "Thread 1");
        Thread thread2 = new Thread(threadGroup1, task, "Thread 2");
        Thread thread3 = new Thread(threadGroup1, task, "Thread 3");
profile
알고리즘, 자료구조 블로그: https://gyun97.github.io/

0개의 댓글